Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughIntroduces ApiExecutor feature enabling raw HTTP requests against OpenFGA APIs without typed SDK methods. Adds public classes ApiExecutor, ApiExecutorRequestBuilder, and ApiResponse. Includes internal ApiClient method for response wrapper support and public GetApiExecutor() on OpenFgaClient. Adds comprehensive example project and tests. Changes
Sequence DiagramsequenceDiagram
participant Client as OpenFgaClient
participant Executor as ApiExecutor
participant Builder as ApiExecutorRequestBuilder
participant ApiClient as ApiClient
participant HTTP as HttpClient
participant Response as ApiResponse
Client->>Client: GetApiExecutor() creates lazy instance
Client->>Executor: new ApiExecutor(apiClient, config)
Note over Client,Builder: Build Request
Builder->>Builder: Of(method, path)
Builder->>Builder: PathParam/QueryParam/Header/Body
Builder->>Builder: Build() validates
Note over Executor,HTTP: Send Request
Executor->>Executor: SendAsync(request)
Executor->>ApiClient: SendRequestWithWrapperAsync<T>()
ApiClient->>ApiClient: BuildHeaders (merge defaults + custom)
ApiClient->>HTTP: Send with auth, retries
HTTP-->>ApiClient: HttpResponseMessage
ApiClient-->>Executor: ResponseWrapper<T>
Note over Executor,Response: Process Response
Executor->>Response: FromHttpResponse(message, raw, data)
Response-->>Executor: ApiResponse<T>
Executor-->>Client: ApiResponse<T> with StatusCode, Headers, Data
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
There was a problem hiding this comment.
Pull request overview
This PR introduces an ApiExecutor feature that allows developers to make custom API requests to OpenFGA endpoints using a fluent builder pattern. The implementation provides both typed and raw JSON response handling while automatically leveraging the SDK's authentication, retry logic, and error handling infrastructure.
Changes:
- Added
ApiExecutorclass for executing arbitrary API requests with automatic auth/retry handling - Added
ApiExecutorRequestBuilderfor fluent request construction with path params, query params, headers, and body - Added
ApiResponse<T>class to encapsulate response data including status, headers, raw response, and typed data - Exposed internal
ApiClientthroughOpenFgaApito enableApiExecutorfunctionality - Added comprehensive unit tests, integration tests, and documentation
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 31 comments.
Show a summary per file
| File | Description |
|---|---|
| src/OpenFga.Sdk/Client/Client.cs | Adds ApiExecutor property with lazy initialization and disposal handling |
| src/OpenFga.Sdk/Client/ApiExecutor/ApiResponse.cs | Response wrapper providing status, headers, raw and typed response data |
| src/OpenFga.Sdk/Client/ApiExecutor/ApiExecutorRequestBuilder.cs | Fluent builder for constructing API requests with validation |
| src/OpenFga.Sdk/Client/ApiExecutor/ApiExecutor.cs | Executes requests with typed or raw responses using SDK infrastructure |
| src/OpenFga.Sdk/ApiClient/ApiClient.cs | Adds SendRequestWithWrapperAsync to expose raw response alongside typed data |
| src/OpenFga.Sdk/Api/OpenFgaApi.cs | Exposes internal ApiClient for ApiExecutor usage |
| src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj | Adds Testcontainers dependency for integration testing |
| src/OpenFga.Sdk.Test/Client/ApiExecutor/*.cs | Comprehensive unit and integration tests for all ApiExecutor components |
| README.md | Documents ApiExecutor usage with examples and feature descriptions |
| CHANGELOG.md | Records new ApiExecutor feature in unreleased section |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/OpenFga.Sdk.Test/Client/ApiExecutor/ApiExecutorIntegrationTests.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 101 out of 102 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/OpenFga.Sdk.Test/Client/ApiExecutor/ApiExecutorIntegrationTests.cs
Outdated
Show resolved
Hide resolved
|
Documentation Updates 2 document(s) were updated by changes in this PR: Custom HTTP Headers SupportView Changes@@ -55,6 +55,86 @@
- All API methods accept an optional `IRequestOptions` parameter for per-request headers.
[Source](https://github.com/openfga/dotnet-sdk/blob/97552d60fd779db36836abeaa0e2bb9d58f87c70/CHANGELOG.md#L11-L60)
+
+#### Calling Other Endpoints with ApiExecutor
+For advanced use cases where you need to call API endpoints not yet available in the SDK's typed methods, or when you need access to full response details (status code, headers, raw response), you can use `ApiExecutor`.
+
+**Basic Usage:**
+```csharp
+using OpenFga.Sdk.ApiClient;
+
+var client = new OpenFgaClient(configuration);
+var executor = client.ApiExecutor;
+
+// Build a request using RequestBuilder
+var request = new RequestBuilder<object> {
+ Method = HttpMethod.Get,
+ BasePath = configuration.ApiUrl,
+ PathTemplate = "/stores/{store_id}",
+ PathParameters = new Dictionary<string, string> { { "store_id", storeId } },
+ QueryParameters = new Dictionary<string, string>()
+};
+
+// Execute and get full response details
+var response = await executor.ExecuteAsync<object, GetStoreResponse>(request, "GetStore");
+
+// Always check if the request was successful
+if (!response.IsSuccessful) {
+ Console.WriteLine($"Request failed: {response.StatusCode}");
+ return;
+}
+
+// Access response details
+Console.WriteLine($"Status: {response.StatusCode}");
+Console.WriteLine($"Headers: {response.Headers.Count}");
+Console.WriteLine($"Data: {response.Data.Name}");
+```
+
+**Custom Headers with ApiExecutor:**
+```csharp
+var options = new ClientRequestOptions {
+ Headers = new Dictionary<string, string> {
+ { "X-Custom-Header", "value" },
+ { "X-Trace-Id", traceId }
+ }
+};
+
+var response = await executor.ExecuteAsync<object, TResponse>(
+ request,
+ "CustomEndpoint",
+ options
+);
+```
+
+**Fluent API Style:**
+```csharp
+var request = RequestBuilder<object>
+ .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/check")
+ .WithPathParameter("store_id", storeId)
+ .WithQueryParameter("consistency", "HIGHER_CONSISTENCY")
+ .WithBody(checkRequest);
+
+var response = await executor.ExecuteAsync<object, CheckResponse>(request, "Check");
+```
+
+**Key Features:**
+- Full access to response details: status code, headers, raw JSON response, and typed data
+- Integrates with SDK's authentication, retry logic, and error handling
+- Supports custom headers via ClientRequestOptions
+- Fluent API for building requests
+- Type-safe request and response handling
+
+**Response Object (ApiResponse):**
+The `ApiResponse<T>` object provides:
+- `IsSuccessful` - Boolean indicating if the request succeeded
+- `StatusCode` - HTTP status code
+- `Headers` - Response headers
+- `RawResponse` - Raw JSON response string
+- `Data` - Typed response object (when successful)
+
+For more examples, see the [ApiExecutor Example](https://github.com/openfga/dotnet-sdk/tree/main/example/ApiExecutorExample/).
+
+**Note:** This feature replaces the deprecated `SendRequestAsync` method.
---
StreamedListObjects Feature OverviewView Changes@@ -5,7 +5,7 @@
| Python | Yes | Async generator | [Example](https://github.com/openfga/python-sdk/blob/fd04bc4b8525e536208e2091dce16edf2f220250/example/streamed-list-objects/README.md) | Both async and sync versions. Yields results as they arrive. |
| Go | Yes | Channel | [Example](https://github.com/openfga/go-sdk/blob/main/example/streamed_list_objects/main.go) | The `StreamedListObjects` method is public and available for use. Streams results using channels. |
| JS | Yes (>= v0.9.3) | Async iterator | [Documentation](#streamed-list-objects) | Supported as of v0.9.3. Streams results using async iterators. |
-| .NET | No | N/A | [Issue](https://github.com/openfga/dotnet-sdk/issues/110) | Feature request open. Implementation in progress, release pending review. |
+| .NET | Via ApiExecutor | Request builder | [Example](https://github.com/openfga/dotnet-sdk/tree/main/example/ApiExecutorExample/) | Use `ApiExecutor` to call `/streamed-list-objects` endpoint. No dedicated typed method yet. |
## Feature Details
@@ -102,7 +102,44 @@
```
### .NET SDK
-The .NET SDK does not currently support StreamedListObjects. There is an open feature request and ongoing work to add support ([.NET issue](https://github.com/openfga/dotnet-sdk/issues/110)). The current workaround is to call the `/streamed-list-objects` API directly.
+The .NET SDK provides `ApiExecutor` for calling custom API endpoints, including `/streamed-list-objects`. While there is no dedicated typed method for StreamedListObjects yet, you can use ApiExecutor to call the endpoint directly:
+
+**Example:**
+```csharp
+using OpenFga.Sdk.ApiClient;
+
+var client = new OpenFgaClient(configuration);
+var executor = client.ApiExecutor;
+
+// Build request to call /streamed-list-objects
+var request = RequestBuilder<ListObjectsRequest>
+ .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/streamed-list-objects")
+ .WithPathParameter("store_id", storeId)
+ .WithBody(new ListObjectsRequest {
+ Type = "document",
+ Relation = "viewer",
+ User = "user:anne"
+ });
+
+// Execute the request
+var response = await executor.ExecuteAsync<ListObjectsRequest, StreamedListObjectsResponse>(
+ request,
+ "StreamedListObjects"
+);
+
+if (response.IsSuccessful) {
+ Console.WriteLine($"Objects: {string.Join(", ", response.Data.Objects)}");
+}
+```
+
+**Key Points:**
+- `ApiExecutor` provides a lower-level API for calling any OpenFGA endpoint
+- Integrates with SDK authentication, retry logic, and error handling
+- Supports custom headers via `ClientRequestOptions`
+- Provides full response details: status code, headers, and typed data
+- Similar to Java's `StreamingApiExecutor` and JS's `executeApiRequest` for custom endpoint access
+
+For more details on using ApiExecutor, see the [ApiExecutor Example](https://github.com/openfga/dotnet-sdk/tree/main/example/ApiExecutorExample/).
## Benefits Over ListObjects
StreamedListObjects removes the 1000-object pagination limit, streams results as they are available, reduces memory usage, enables early termination, and is better suited for large or computed result sets ([Java example](https://github.com/openfga/java-sdk/blob/main/examples/api-executor/README.md), [Go example](https://github.com/openfga/go-sdk/blob/e6dd3e6c12ecb6654531d5febe80404db4018c26/example/streamed_list_objects/README.md)).
@@ -111,5 +148,6 @@
- [Java SDK StreamingApiExecutor Example](https://github.com/openfga/java-sdk/blob/main/examples/api-executor/README.md)
- [Python SDK StreamedListObjects Example](https://github.com/openfga/python-sdk/blob/fd04bc4b8525e536208e2091dce16edf2f220250/example/streamed-list-objects/README.md)
- [Go SDK StreamedListObjects Example](https://github.com/openfga/go-sdk/blob/e6dd3e6c12ecb6654531d5febe80404db4018c26/example/streamed_list_objects/README.md)
+- [.NET SDK ApiExecutor Example](https://github.com/openfga/dotnet-sdk/tree/main/example/ApiExecutorExample/)
- [JS SDK Feature Request](https://github.com/openfga/js-sdk/issues/236)
- [.NET SDK Feature Request](https://github.com/openfga/dotnet-sdk/issues/110) |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@example/ApiExecutorExample/Makefile`:
- Line 1: Add the missing run-all target to the .PHONY declaration so the
Makefile treats the run-all target as phony; update the .PHONY line (which
currently lists help start-openfga stop-openfga run clean) to include run-all to
prevent accidental conflicts with a filesystem entry named "run-all".
In `@example/ApiExecutorExample/README.md`:
- Line 66: Update the fenced code block that contains "=== OpenFGA ApiExecutor
Example ===" to include a language specifier (e.g., add ```text or ```console)
so the block renders consistently; locate the markdown fenced block in README.md
and replace the opening ``` with ```text (or ```console) ensuring the closing
``` remains unchanged.
In `@src/OpenFga.Sdk/Api/OpenFgaApi.cs`:
- Around line 44-48: The ApiClientInternal property will be overwritten by
codegen; remove it from the generated OpenFgaApi class and instead add it to a
new partial class declaration (e.g., class OpenFgaApi in OpenFgaApi.partial.cs)
in the same namespace so the generated class and your partial class merge at
compile time; implement the internal ApiClientInternal => _apiClient property in
that partial file and ensure the new file is kept outside the generator's
overwrite scope (or add the generated file to the generator ignore list if you
intend to fully manage it manually).
🧹 Nitpick comments (6)
example/README.md (1)
7-18: Documentation additions look good.Minor grammar nit: "bare bones" on line 8 should be hyphenated as "bare-bones" when used as a compound adjective.
📝 Suggested fix
**Example 1:** -A bare bones example. It creates a store, and runs a set of calls against it including creating a model, writing tuples and checking for access. +A bare-bones example. It creates a store, and runs a set of calls against it including creating a model, writing tuples and checking for access.src/OpenFga.Sdk/ApiClient/ApiClient.cs (1)
127-153: Header validation is bypassed for additionalHeaders.The
SendRequestWithWrapperAsyncmethod callsBuildHeaderswithnullfor options (line 135), then manually mergesadditionalHeaders(lines 138-142). This bypasses theConfiguration.ValidateHeaderscall that occurs inBuildHeadersfor per-request headers.Additionally, the manual merge allows
additionalHeadersto override theAuthorizationheader set by the SDK's authentication flow. While this may be intentional for advanced use cases, it could also inadvertently allow callers to corrupt authentication.Consider:
- Validating
additionalHeadersbefore merging- Preventing override of security-sensitive headers like
Authorizationunless explicitly intended♻️ Suggested validation
internal async Task<ResponseWrapper<TRes>> SendRequestWithWrapperAsync<TReq, TRes>( RequestBuilder<TReq> requestBuilder, string apiName, IDictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default) { var sw = Stopwatch.StartNew(); var authToken = await GetAuthenticationTokenAsync(apiName); var mergedHeaders = BuildHeaders(_configuration, authToken, null); + // Validate additional headers + Configuration.Configuration.ValidateHeaders(additionalHeaders, "additionalHeaders"); + // Merge additional headers (from ApiExecutor) with auth headers if (additionalHeaders != null) { foreach (var header in additionalHeaders) { + // Optionally protect Authorization header from being overwritten + // if (string.Equals(header.Key, "Authorization", StringComparison.OrdinalIgnoreCase)) continue; mergedHeaders[header.Key] = header.Value; } }src/OpenFga.Sdk/Client/Client.cs (1)
61-66: Minor: Redundant null-conditional operator.On line 63,
_apiExecutor.Value?.Dispose()uses a null-conditional operator, butLazy<T>.Valuewill never be null whenIsValueCreatedis true. This is harmless but slightly misleading.🔧 Optional simplification
public void Dispose() { if (_apiExecutor.IsValueCreated) { - _apiExecutor.Value?.Dispose(); + _apiExecutor.Value.Dispose(); } api.Dispose(); }example/ApiExecutorExample/Makefile (1)
17-19: Fixed sleep duration may cause flaky behavior.A fixed 3-second sleep may not be enough on slower systems or may be unnecessarily long on fast systems. Consider a retry loop with the health check instead.
🔧 Suggested improvement with retry loop
start-openfga: `@echo` "Starting OpenFGA server on localhost:8080..." `@docker` run -d --name openfga-example -p 8080:8080 openfga/openfga:latest run `@echo` "Waiting for OpenFGA to be ready..." - `@sleep` 3 - `@curl` -s http://localhost:8080/healthz || (echo "OpenFGA failed to start" && exit 1) + `@for` i in 1 2 3 4 5 6 7 8 9 10; do \ + curl -s http://localhost:8080/healthz && break || sleep 1; \ + done || (echo "OpenFGA failed to start within 10 seconds" && exit 1) `@echo` "✅ OpenFGA is ready!"src/OpenFga.Sdk/Client/ApiExecutor/ApiExecutor.cs (1)
83-115: Non-genericSendAsyncwill fail if response is not valid JSON.The use of
JsonElementas an intermediate type means deserialization is attempted, which will throw if the response body is not valid JSON (e.g., plain text error messages or empty responses). If the goal is truly "raw" response handling, consider catching deserialization errors or using a different approach that doesn't require JSON parsing.src/OpenFga.Sdk.Test/Client/ApiExecutor/ApiExecutorTests.cs (1)
298-327: Consider using mocked HttpClient for consistency.These tests create
OpenFgaClientwithout a mockedHttpClient. While they work correctly (the null request test throws before network access, and the singleton test doesn't make requests), using mocked clients would be more consistent with the other tests and prevent any accidental network calls if the implementation changes.
There was a problem hiding this comment.
This SDK already includes an early version of the API executor called RequestBuilder + SendRequestAsync that handles the retry logic, auth and standardizes the interface, e.g.
var requestBuilder = new RequestBuilder<BatchCheckRequest> {
Method = new HttpMethod("POST"),
BasePath = _configuration.BasePath,
PathTemplate = "/stores/{store_id}/batch-check",
PathParameters = pathParams,
Body = body,
QueryParameters = queryParams,
};
return await _apiClient.SendRequestAsync<BatchCheckRequest, BatchCheckResponse>(requestBuilder,
"BatchCheck", options, cancellationToken);
What the API Executor should be doing is either building on top of that work fully or replacing it. Currently, it is building on top of RequestBuilder, but not fully.
It is duplicating the logic by introducing ApiExecutorRequestBuilder, that is separate from RequestBuilder, duplicating some of the work.
It should just be an improved iteration/abstraction on top of RequestBuilder + SendRequestAsync.
Basically what I expect to see is:
- OpenFgaApi using ApiExecutor for all API interactions instead of directly calling RequestBuilder (if it offers a material improvement), otherwise enhancing RequestBuilder to include what is needed.
- The ApiExecutor should be part of the ApiClient, rather than Client. The difference is that ApiClient is the core building block that both OpenFgaApi and Client share and the executor belong there alongside/replacing RequestBuilder
- I would expect it to have a streaming variant that works for StreamedListObjects too, but I'm OK with that being a separate PR
|
@rhamzeh
|
rhamzeh
left a comment
There was a problem hiding this comment.
One more minor thing @SoulPancake - can the OpenFgaApi use the api executor completely? That way it functions as a sort of example.
I don't think we still need SendRequestAsync with the executor available?
Generated from SDK generator PR : openfga/sdk-generator#676
Description
What problem is being solved?
How is it being solved?
What changes are made to solve it?
References
Review Checklist
mainSummary by CodeRabbit
New Features
Documentation
Tests