Skip to content

Commit 8483107

Browse files
feat: Implement server-side BatchCheck using /batch-check endpoint (#150)
* feat: Implement server-side BatchCheck using /batch-check endpoint Implements server-side batch check functionality to address issue #94. Changes: - Add new BatchCheck() method using the server-side /batch-check API endpoint - Rename existing BatchCheck() to ClientBatchCheck() (fully supported, not deprecated) - Add ClientBatchCheckRequest, ClientBatchCheckItem models for user-friendly API - Add ClientBatchCheckResponse, ClientBatchCheckSingleResponse for result mapping - Add ClientUtils helper methods (UUID correlation ID generation, list chunking, transformations) - Add MaxBatchSize property to IClientBatchCheckOptions (default: 50) - Update ListRelations to use renamed ClientBatchCheck method Features: - Auto-generates UUID correlation IDs (cross-SDK compatible with JS SDK) - Validates correlation IDs are unique - Chunks requests by MaxBatchSize (default: 50 checks per batch) - Executes batches in parallel with MaxParallelRequests (default: 10) - Supports all target frameworks (netstandard2.0, net48, net8.0, net9.0) - Returns empty result for empty checks (matches JS SDK behavior) - Fail-fast error handling Tests: - Add 18 unit tests for new models and utilities - Update existing tests to use renamed ClientBatchCheck method - Integration tested against live OpenFGA server Fixes #94 * fix: Address PR feedback for BatchCheck implementation Addresses CodeRabbit and CodeQL feedback from PR #150. Changes: - Add validation to prevent MaxBatchSize <= 0 and MaxParallelRequests <= 0 - Prevents potential deadlock in NETSTANDARD2.0/NET48 builds - Throws FgaValidationError with descriptive message - Create IClientServerBatchCheckOptions for server-side BatchCheck - Includes MaxBatchSize and MaxParallelRequests properties - Removes MaxBatchSize from IClientBatchCheckOptions (unused by ClientBatchCheck) - Removes MaxBatchSize from ClientListRelationsOptions (unused by ListRelations) - Fix Equals(object) methods to use exact type checking - Changed from 'is' pattern to obj.GetType() == this.GetType() - Prevents improper equality checks with subclasses (CodeQL warning) - Applied to ClientBatchCheckItem, ClientBatchCheckRequest, ClientBatchCheckSingleResponse, ClientBatchCheckResponse - Update CHANGELOG.md with unreleased features All fixes validated with no linter errors. * docs: update BatchCheck README to match JS SDK format - Update BatchCheck example to show new server-side API - Use ClientBatchCheckRequest with ClientBatchCheckItem - Show correlation ID generation and filtering by ID - Add MaxBatchSize option example - Simplify example for clarity (2 checks instead of 4) - Add note about ClientBatchCheck() for older servers - Align with JS SDK README structure Addresses feedback from PR #150 * refactor: remove unused ChunkArray method - Remove ChunkArray<T> method (not used anywhere) - Keep ChunkList<T> which is actively used by BatchCheck Addresses feedback from PR #150 * perf: use Enumerable.Chunk for .NET 6+ in ChunkList - Use built-in Enumerable.Chunk for .NET 6.0 and greater - Fall back to manual implementation for older frameworks - Improves performance on modern .NET versions Addresses feedback from PR #150 * test: add integration test for BatchCheck bulk request ID header - Verify X-OpenFGA-Client-Bulk-Request-Id header is present - Validate header value is a valid GUID - Add missing FgaConstants using statement - Follows existing header test patterns Addresses feedback from PR #150 * refactor: use discard for unused mockHandler variable - Change mockHandler to _ in BatchCheck header test - Unused variable doesn't need to be captured - Improves code cleanliness Addresses code review feedback * refactor: rename options classes to match Java SDK naming - Rename ClientServerBatchCheckOptions to ClientBatchCheckOptions - Create ClientBatchCheckClientOptions for client-side ClientBatchCheck() - ClientBatchCheckOptions (server-side) has MaxBatchSize + MaxParallelRequests - ClientBatchCheckClientOptions (client-side) has only MaxParallelRequests - Update ClientListRelationsOptions to inherit from IClientBatchCheckClientOptions - Fix README casing: batchCheck -> BatchCheck, correlationId -> CorrelationId - Update all tests to use correct option types Addresses PR feedback from @ewanharris and @curfew-marathon * fix: address PR feedback for BatchCheck implementation Address review feedback from PR #150: 1. Use hash code constants instead of magic numbers - Replace hardcoded 9661/9923 with FgaConstants references - Applied to all GetHashCode() methods in batch check models 2. Reduce code duplication in parallel processing - Extract ProcessBatchAsync() helper method - Share logic between NET6+ and older framework paths 3. Update documentation - Add OpenFGA server v1.8.0+ requirement to CHANGELOG - Regenerate README with correct examples from updated templates - Fix response property names and model usage All tests passing (274/274 on .NET 9.0). * feat: update equals * fix: revert unnecessary readme changes * feat: address comments --------- Co-authored-by: Anurag Bandyopadhyay <angbpy@gmail.com>
1 parent 7fe66c0 commit 8483107

File tree

97 files changed

+1280
-245
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+1280
-245
lines changed

.openapi-generator-ignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
appveyor.yml
22
git_push.sh
33
api/openapi.yaml
4+
src/OpenFga.Sdk/ApiClient.cs
5+
src/OpenFga.Sdk/Client/*
46
src/OpenFga.Sdk.Test/Api/*
57
src/OpenFga.Sdk.Test/Client/*
68
src/OpenFga.Sdk.Test/Model/*
7-
src/OpenFga.Sdk/ApiClient.cs
8-
src/OpenFga.Sdk/Client/*
9+
src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj
10+
src/OpenFga.Sdk/OpenFga.Sdk.csproj

.openapi-generator/FILES

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ docs/WriteAuthorizationModelResponse.md
9696
docs/WriteRequest.md
9797
docs/WriteRequestDeletes.md
9898
docs/WriteRequestWrites.md
99-
src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj
10099
src/OpenFga.Sdk/Api/OpenFgaApi.cs
101100
src/OpenFga.Sdk/Constants/FgaConstants.cs
102101
src/OpenFga.Sdk/Model/AbortedMessageResponse.cs
@@ -155,7 +154,6 @@ src/OpenFga.Sdk/Model/ReadResponse.cs
155154
src/OpenFga.Sdk/Model/RelationMetadata.cs
156155
src/OpenFga.Sdk/Model/RelationReference.cs
157156
src/OpenFga.Sdk/Model/RelationshipCondition.cs
158-
src/OpenFga.Sdk/Model/RequestOptions.cs
159157
src/OpenFga.Sdk/Model/SourceInfo.cs
160158
src/OpenFga.Sdk/Model/Status.cs
161159
src/OpenFga.Sdk/Model/Store.cs
@@ -187,4 +185,3 @@ src/OpenFga.Sdk/Model/WriteAuthorizationModelResponse.cs
187185
src/OpenFga.Sdk/Model/WriteRequest.cs
188186
src/OpenFga.Sdk/Model/WriteRequestDeletes.cs
189187
src/OpenFga.Sdk/Model/WriteRequestWrites.cs
190-
src/OpenFga.Sdk/OpenFga.Sdk.csproj

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## [Unreleased](https://github.com/openfga/dotnet-sdk/compare/v0.8.0...HEAD)
44

5+
### Added
6+
- feat: add server-side `BatchCheck()` method using `/batch-check` API endpoint
7+
- See [Batch Check documentation](README.md#batch-check) for usage examples and configuration
8+
9+
### Changed
10+
- **BREAKING**: Existing `BatchCheck()` renamed to `ClientBatchCheck()`
11+
- New server-side `BatchCheck()` method requires [OpenFGA server v1.8.0+](https://github.com/openfga/openfga/releases/tag/v1.8.0)
12+
- For configuration options and behavior details, see [README documentation](README.md#batch-check)
13+
14+
### Fixed
515
- fix: ApiToken credentials no longer cause reserved header exception (#146)
616

717
## v0.8.0

README.md

Lines changed: 108 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ This is an autogenerated SDK for OpenFGA. It provides a wrapper around the [Open
3838
- [Relationship Queries](#relationship-queries)
3939
- [Check](#check)
4040
- [Batch Check](#batch-check)
41+
- [Client Batch Check](#client-batch-check)
4142
- [Expand](#expand)
4243
- [List Objects](#list-objects)
4344
- [List Relations](#list-relations)
@@ -642,8 +643,15 @@ var response = await fgaClient.Check(body, options);
642643

643644
##### Batch Check
644645

645-
Run a set of [checks](#check). Batch Check will return `allowed: false` if it encounters an error, and will return the error in the body.
646-
If 429s or 5xxs are encountered, the underlying check will retry up to 3 times before giving up.
646+
Similar to [check](#check), but instead of checking a single user-object relationship, accepts a list of relationships to check. Requires OpenFGA server version [1.8.0](https://github.com/openfga/openfga/releases/tag/v1.8.0) or greater.
647+
648+
This method automatically handles:
649+
- Generating correlation IDs for checks that don't have one
650+
- Validating that correlation IDs are unique
651+
- Chunking requests based on `MaxBatchSize` (default: 50)
652+
- Executing batches in parallel based on `MaxParallelRequests` (default: 10)
653+
654+
If 429s or 5xxs are encountered, the underlying requests will retry up to 3 times before giving up.
647655

648656
> **Note**: The order of `BatchCheck` results is not guaranteed to match the order of the checks provided. Use `correlationId` to pair responses with requests.
649657
@@ -652,87 +660,139 @@ var options = new ClientBatchCheckOptions {
652660
// You can rely on the model id set in the configuration or override it for this specific request
653661
AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1",
654662
MaxParallelRequests = 5, // Max number of requests to issue in parallel, defaults to 10
663+
MaxBatchSize = 20, // Max number of checks per batch request, defaults to 50
655664
};
656-
var body = new List<ClientCheckRequest>(){
657-
new() {
658-
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
659-
Relation = "viewer",
660-
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
661-
ContextualTuples = new List<ClientTupleKey>() {
662-
new() {
663-
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
664-
Relation = "editor",
665-
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
666-
}
665+
var body = new ClientBatchCheckRequest {
666+
Checks = new List<ClientBatchCheckItem>() {
667+
new() {
668+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
669+
Relation = "viewer",
670+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
671+
ContextualTuples = new ContextualTupleKeys {
672+
TupleKeys = new List<TupleKey>() {
673+
new() {
674+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
675+
Relation = "editor",
676+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
677+
}
678+
}
679+
},
680+
// CorrelationId is optional - will be auto-generated if not provided
667681
},
668-
},
669-
new() {
670-
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
671-
Relation = "admin",
672-
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
673-
ContextualTuples = new List<ClientTupleKey>() {
674-
new() {
675-
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
676-
Relation = "editor",
677-
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
678-
}
682+
new() {
683+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
684+
Relation = "admin",
685+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
686+
ContextualTuples = new ContextualTupleKeys {
687+
TupleKeys = new List<TupleKey>() {
688+
new() {
689+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
690+
Relation = "editor",
691+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
692+
}
693+
}
694+
},
695+
CorrelationId = "check-admin-01234", // Custom correlation ID
679696
},
680-
},
681-
new() {
682-
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
683-
Relation = "creator",
684-
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
685-
},
686-
new() {
687-
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
688-
Relation = "deleter",
689-
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
697+
new() {
698+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
699+
Relation = "creator",
700+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
701+
},
702+
new() {
703+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
704+
Relation = "deleter",
705+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
706+
}
690707
}
691708
};
692709

693710
var response = await fgaClient.BatchCheck(body, options);
694711

695712
/*
696-
response.Responses = [{
713+
response.Result = [{
714+
CorrelationId: "01JA8PM3QM7VBPGB8KMPK8SBD5", // Auto-generated
697715
Allowed: false,
698716
Request: {
699717
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
700718
Relation: "viewer",
701719
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
702-
ContextualTuples: [{
703-
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
704-
Relation: "editor",
705-
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"
706-
}]
720+
ContextualTuples: { ... }
707721
}
708722
}, {
723+
CorrelationId: "check-admin-01234", // Custom ID
709724
Allowed: false,
710725
Request: {
711726
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
712727
Relation: "admin",
713728
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
714-
ContextualTuples: [{
715-
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
716-
Relation: "editor",
717-
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"
718-
}]
729+
ContextualTuples: { ... }
719730
}
720731
}, {
732+
CorrelationId: "01JA8PMM6A90NV5ET0F28CYSZQ",
721733
Allowed: false,
722734
Request: {
723735
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
724736
Relation: "creator",
725737
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
726738
},
727-
Error: <FgaError ...>
739+
Error: { Message: "...", InputError: "..." } // Error details if check failed
728740
}, {
741+
CorrelationId: "01JA8PN7K2XBVW9R3FQMH5JZTA",
729742
Allowed: true,
730743
Request: {
731744
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
732745
Relation: "deleter",
733746
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
734-
}},
735-
]
747+
}
748+
}]
749+
*/
750+
```
751+
752+
##### Client Batch Check
753+
754+
Run a set of [checks](#check) by making individual `/check` API calls in parallel on the client side. This is useful for small batches (< 10 checks) when you want more control over each individual request.
755+
756+
For larger batches or to use the server-side batch endpoint, use [`BatchCheck`](#batch-check) instead.
757+
758+
```csharp
759+
var options = new ClientBatchCheckClientOptions {
760+
AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1",
761+
MaxParallelRequests = 5, // Max number of requests to issue in parallel, defaults to 10
762+
};
763+
var body = new List<ClientCheckRequest>() {
764+
new() {
765+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
766+
Relation = "viewer",
767+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
768+
},
769+
new() {
770+
User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
771+
Relation = "admin",
772+
Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
773+
}
774+
};
775+
776+
var response = await fgaClient.ClientBatchCheck(body, options);
777+
778+
/*
779+
response.Responses = [{
780+
Allowed: false,
781+
Request: {
782+
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
783+
Relation: "viewer",
784+
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"
785+
},
786+
Error: null
787+
}, {
788+
Allowed: true,
789+
Request: {
790+
User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
791+
Relation: "admin",
792+
Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"
793+
},
794+
Error: null
795+
}]
736796
*/
737797
```
738798

example/Example1/Example1.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,17 +232,42 @@ await fgaClient.Write(new ClientWriteRequest {
232232
});
233233
Console.WriteLine("Allowed: " + checkResponse.Allowed);
234234

235-
// Batch checking for access with context
236-
Console.WriteLine("Batch checking for access with context");
237-
var batchCheckResponse = await fgaClient.BatchCheck(new List<ClientCheckRequest>() {
235+
// Client-side batch checking (individual check calls in parallel)
236+
Console.WriteLine("Client-side batch checking for access with context");
237+
var clientBatchCheckResponse = await fgaClient.ClientBatchCheck(new List<ClientCheckRequest>() {
238238
new () {
239239
User = "user:anne",
240240
Relation = "viewer",
241241
Object = "document:roadmap",
242242
Context = new { ViewCount = 100 }
243243
}
244244
});
245-
Console.WriteLine("Responses[0].Allowed: " + batchCheckResponse.Responses[0].Allowed);
245+
Console.WriteLine("ClientBatchCheck - Responses[0].Allowed: " + clientBatchCheckResponse.Responses[0].Allowed);
246+
247+
// Server-side batch checking (using /batch-check endpoint)
248+
Console.WriteLine("Server-side batch checking for access with context");
249+
var batchCheckResponse = await fgaClient.BatchCheck(new ClientBatchCheckRequest {
250+
Checks = new List<ClientBatchCheckItem>() {
251+
new () {
252+
User = "user:anne",
253+
Relation = "viewer",
254+
Object = "document:roadmap",
255+
Context = new { ViewCount = 100 },
256+
// CorrelationId is optional - will be auto-generated if not provided
257+
},
258+
new () {
259+
User = "user:anne",
260+
Relation = "writer",
261+
Object = "document:roadmap",
262+
Context = new { ViewCount = 100 },
263+
CorrelationId = "custom-correlation-id-123"
264+
}
265+
}
266+
});
267+
Console.WriteLine("BatchCheck - Result count: " + batchCheckResponse.Result.Count);
268+
foreach (var result in batchCheckResponse.Result) {
269+
Console.WriteLine($" CorrelationId: {result.CorrelationId}, Allowed: {result.Allowed}");
270+
}
246271

247272
// Listing relations with context
248273
Console.WriteLine("Listing relations with context");

example/Example1/Example1.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@
99
</PropertyGroup>
1010

1111
<!-- To target the released version, uncomment this section -->
12+
1213
<ItemGroup>
1314
<PackageReference Include="OpenFga.Sdk" Version="0.8.0"><PrivateAssets>all</PrivateAssets></PackageReference>
1415
</ItemGroup>
1516

17+
<!-- To target the local build, use project reference -->
18+
<ItemGroup>
19+
<ProjectReference Include="..\..\src\OpenFga.Sdk\OpenFga.Sdk.csproj" />
20+
</ItemGroup>
21+
1622
<!-- To target the local build, uncomment this section (make sure to build that project first) -->
1723
<!-- <ItemGroup>-->
1824
<!-- <Reference Include="OpenFga.Sdk">-->

0 commit comments

Comments
 (0)