Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4df1d01
Claude Code attempt at .NET Standard 2.0 refactor
ryanpq Apr 21, 2025
ccb6600
Claude Code refactor - remove multi-targeting
ryanpq Apr 21, 2025
ea43b2b
Claude Code refactor - remove remaining unused conditionals
ryanpq Apr 21, 2025
3667ba3
AI Experiment - improve dotNET test coverage Core 3.1, Framework 4.8,…
ryanpq Apr 21, 2025
7e4ff54
Resolve CodeQL identified issues
ryanpq May 8, 2025
c3b75d6
Merge branch 'main' into ai-dotnet-refactor-test
ryanpq May 8, 2025
72c9f85
fix additional codeql issues
ryanpq May 8, 2025
67cd90f
Merge branch 'main' into ai-dotnet-refactor-test
ryanpq May 19, 2025
60e3a6b
fixes after merging from main
ryanpq May 19, 2025
20f1d17
fix: addressing telemetry issues
ryanpq May 20, 2025
7bb353d
cont telemetry fixes
ryanpq May 20, 2025
7c4efae
cont telemetry fixes
ryanpq May 20, 2025
872dd40
cont telemetry fixes
ryanpq May 20, 2025
8ca1a78
cont telemetry fixes
ryanpq May 20, 2025
a7e11aa
Revert "cont telemetry fixes"
ryanpq May 20, 2025
1c938e6
Revert "cont telemetry fixes"
ryanpq May 20, 2025
ab6f74c
Revert "cont telemetry fixes"
ryanpq May 20, 2025
ac9bbf3
Revert "fix: addressing telemetry issues"
ryanpq May 20, 2025
316bf7d
fixes to AI code
ryanpq May 21, 2025
0150918
fixes to AI code
ryanpq May 21, 2025
401d58f
fixes to AI code
ryanpq May 21, 2025
91c8115
Revert "fixes to AI code"
ryanpq May 21, 2025
a8553d1
Revert "fixes to AI code"
ryanpq May 21, 2025
0c3b62c
Revert "fixes to AI code"
ryanpq May 21, 2025
4b9c73f
Revert "fixes after merging from main"
ryanpq May 21, 2025
a49e0b8
liststores fix
ryanpq May 21, 2025
8565886
Revert "liststores fix"
ryanpq May 21, 2025
aa15f39
update api hash
ryanpq May 21, 2025
4d0cda1
Update main.yaml
ryanpq May 21, 2025
84e1ea6
Update Makefile
ryanpq May 21, 2025
776cf37
address opentelemetry compatibility
ryanpq May 21, 2025
73ce815
Revert API hash in makefile so other languages do not fail tests
ryanpq May 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ OPENAPI_GENERATOR_CLI_DOCKER_TAG = v6.4.0
NODE_DOCKER_TAG = 20-alpine
GO_DOCKER_TAG = 1
DOTNET_DOCKER_TAG = 6.0
DOTNET_TEST_IMAGE = openfga-dotnet-test:latest
GOLINT_DOCKER_TAG = latest-alpine
BUSYBOX_DOCKER_TAG = 1
GRADLE_DOCKER_TAG = 8.2
Expand Down Expand Up @@ -36,6 +37,7 @@ pull-docker-images:
docker pull mcr.microsoft.com/dotnet/sdk:${DOTNET_DOCKER_TAG}
docker pull busybox:${BUSYBOX_DOCKER_TAG}
docker pull gradle:${GRADLE_DOCKER_TAG}
docker build -t ${DOTNET_TEST_IMAGE} -f docker/dotnet-test.Dockerfile .

## Building and Testing
.PHONY: test
Expand Down Expand Up @@ -89,9 +91,13 @@ build-client-go:
tag-client-dotnet: test-client-dotnet
make utils-tag-client sdk_language=dotnet

.PHONY: build-dotnet-test-image
build-dotnet-test-image:
docker build -t ${DOTNET_TEST_IMAGE} -f docker/dotnet-test.Dockerfile .

.PHONY: test-client-dotnet
test-client-dotnet: build-client-dotnet
make run-in-docker sdk_language=dotnet image=mcr.microsoft.com/dotnet/sdk:${DOTNET_DOCKER_TAG} command="/bin/sh -c 'dotnet test'"
test-client-dotnet: build-client-dotnet build-dotnet-test-image
make run-in-docker sdk_language=dotnet image=${DOTNET_TEST_IMAGE} command="/bin/sh -c 'make test && make test-netcore31 && make test-framework'"

.PHONY: build-client-dotnet
build-client-dotnet:
Expand Down
54 changes: 52 additions & 2 deletions config/clients/dotnet/config.overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,39 @@
"releaseNote": "",
"sortParamsByRequiredFlag": true,
"sourceFolder": "src",
"targetFramework": "net6.0",
"targetFramework": "netstandard2.0",
"multiTarget": false,
"validatable": true,
"enablePostProcessFile": true,
"hashCodeBasePrimeNumber": 9661,
"hashCodeMultiplierPrimeNumber": 9923,
"supportsOpenTelemetry": true,
"testTargetFramework": "net6.0",
"files": {
"TestHelper.mustache": {
"destinationFilename": "src/OpenFga.Sdk.Test/Helpers/FrameworkCompat.cs",
"templateType": "SupportingFiles"
},
"FrameworkCompatibilityTests.mustache": {
"destinationFilename": "src/OpenFga.Sdk.Test/FrameworkCompatibility/FrameworkTests.cs",
"templateType": "SupportingFiles"
},
"netframework_testproject.mustache": {
"destinationFilename": "src/OpenFga.Sdk.Test.Framework/OpenFga.Sdk.Test.Framework.csproj",
"templateType": "SupportingFiles"
},
"FrameworkCompatibilityTest.mustache": {
"destinationFilename": "src/OpenFga.Sdk.Test.Framework/FrameworkCompatibilityTests.cs",
"templateType": "SupportingFiles"
},
"netcore31_testproject.mustache": {
"destinationFilename": "src/OpenFga.Sdk.Test.NetCore31/OpenFga.Sdk.Test.NetCore31.csproj",
"templateType": "SupportingFiles"
},
"NetCore31CompatibilityTest.mustache": {
"destinationFilename": "src/OpenFga.Sdk.Test.NetCore31/NetCore31CompatibilityTests.cs",
"templateType": "SupportingFiles"
},
"Client_OAuth2Client.mustache": {
"destinationFilename": "src/OpenFga.Sdk/ApiClient/OAuth2Client.cs",
"templateType": "SupportingFiles"
Expand Down Expand Up @@ -67,6 +93,14 @@
"destinationFilename": "src/OpenFga.Sdk/Client/ClientConfiguration.cs",
"templateType": "SupportingFiles"
},
"Client/ClientExtensions.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Client/ClientExtensions.cs",
"templateType": "SupportingFiles"
},
"Client/DictionaryExtensions.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Client/DictionaryExtensions.cs",
"templateType": "SupportingFiles"
},
"Client/Model/AuthorizationModelIdOptions.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Client/Model/AuthorizationModelIdOptions.cs",
"templateType": "SupportingFiles"
Expand Down Expand Up @@ -255,6 +289,10 @@
"destinationFilename": "src/OpenFga.Sdk/Telemetry/Metrics.cs",
"templateType": "SupportingFiles"
},
"Telemetry/Standard20TelemetryTypes.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Telemetry/Standard20TelemetryTypes.cs",
"templateType": "SupportingFiles"
},
"Configuration_Configuration.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Configuration/Configuration.cs",
"templateType": "SupportingFiles"
Expand Down Expand Up @@ -311,6 +349,10 @@
"destinationFilename": "src/OpenFga.Sdk/Exceptions/ValidationError.cs",
"templateType": "SupportingFiles"
},
"Net/HttpStatusCodeExtensions.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Net/HttpStatusCodeExtensions.cs",
"templateType": "SupportingFiles"
},
"Configuration_Credentials.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Configuration/Credentials.cs",
"templateType": "SupportingFiles"
Expand All @@ -331,6 +373,10 @@
"destinationFilename": "OpenTelemetry.md",
"templateType": "SupportingFiles"
},
"GlobalUsings.mustache": {
"destinationFilename": "src/OpenFga.Sdk/GlobalUsings.cs",
"templateType": "SupportingFiles"
},
"example/Makefile": {},
"example/README.md": {},
"example/Example1/Example1.cs": {},
Expand All @@ -339,6 +385,10 @@
"example/OpenTelemetryExample/OpenTelemetryExample.cs": {},
"example/OpenTelemetryExample/OpenTelemetryExample.csproj": {},
"assets/FGAIcon.png": {},
".editorconfig": {}
".editorconfig": {},
"Makefile.mustache": {
"destinationFilename": "Makefile",
"templateType": "SupportingFiles"
}
}
}
100 changes: 91 additions & 9 deletions config/clients/dotnet/template/Client/Client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,12 @@ public class {{appShortName}}Client : IDisposable {

var clientWriteOpts = new ClientWriteOptions() { StoreId = StoreId, AuthorizationModelId = authorizationModelId };

var writeChunks = body.Writes?.Chunk(maxPerChunk).ToList() ?? new List<ClientTupleKey[]>();
var deleteChunks = body.Deletes?.Chunk(maxPerChunk).ToList() ?? new List<ClientTupleKeyWithoutCondition[]>();
var writeChunks = body.Writes == null ? new List<List<ClientTupleKey>>() : body.Writes.Chunk(maxPerChunk).ToList();
var deleteChunks = body.Deletes == null ? new List<List<ClientTupleKeyWithoutCondition>>() : body.Deletes.Chunk(maxPerChunk).ToList();

var writeResponses = new ConcurrentBag<ClientWriteSingleResponse>();
var deleteResponses = new ConcurrentBag<ClientWriteSingleResponse>();
await Parallel.ForEachAsync(writeChunks,
new ParallelOptions { MaxDegreeOfParallelism = maxParallelReqs }, async (request, token) => {
await writeChunks.ForEachAsync(async (request) => {
var writes = request.ToList();
try {
await this.Write(new ClientWriteRequest() { Writes = writes }, clientWriteOpts, cancellationToken);
Expand All @@ -229,6 +228,41 @@ public class {{appShortName}}Client : IDisposable {
});
}
}
catch (FgaApiAuthenticationError e) {
foreach (var tupleKey in writes) {
writeResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiInternalError e) {
foreach (var tupleKey in writes) {
writeResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiValidationError e) {
foreach (var tupleKey in writes) {
writeResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiNotFoundError e) {
foreach (var tupleKey in writes) {
writeResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiRateLimitExceededError e) {
foreach (var tupleKey in writes) {
writeResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (Exception e) {
foreach (var tupleKey in writes) {
writeResponses.Add(new ClientWriteSingleResponse {
Expand All @@ -238,8 +272,7 @@ public class {{appShortName}}Client : IDisposable {
}
});

await Parallel.ForEachAsync(deleteChunks,
new ParallelOptions { MaxDegreeOfParallelism = maxParallelReqs }, async (request, token) => {
await deleteChunks.ForEachAsync(async (request) => {
var deletes = request.ToList();
try {
await this.Write(new ClientWriteRequest() { Deletes = deletes }, clientWriteOpts, cancellationToken);
Expand All @@ -250,6 +283,41 @@ public class {{appShortName}}Client : IDisposable {
});
}
}
catch (FgaApiAuthenticationError e) {
foreach (var tupleKey in deletes) {
deleteResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiInternalError e) {
foreach (var tupleKey in deletes) {
deleteResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiValidationError e) {
foreach (var tupleKey in deletes) {
deleteResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiNotFoundError e) {
foreach (var tupleKey in deletes) {
deleteResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (FgaApiRateLimitExceededError e) {
foreach (var tupleKey in deletes) {
deleteResponses.Add(new ClientWriteSingleResponse {
TupleKey = tupleKey.ToTupleKey(), Status = ClientWriteStatus.FAILURE, Error = e,
});
}
}
catch (Exception e) {
foreach (var tupleKey in deletes) {
deleteResponses.Add(new ClientWriteSingleResponse {
Expand Down Expand Up @@ -307,15 +375,29 @@ public class {{appShortName}}Client : IDisposable {
IClientBatchCheckOptions? options = default,
CancellationToken cancellationToken = default) {
var responses = new ConcurrentBag<BatchCheckSingleResponse>();
await Parallel.ForEachAsync(body,
new ParallelOptions { MaxDegreeOfParallelism = options?.MaxParallelRequests ?? DEFAULT_MAX_METHOD_PARALLEL_REQS }, async (request, token) => {
await body.ForEachAsync(async (request) => {
try {
var response = await Check(request, options, cancellationToken);

responses.Add(new BatchCheckSingleResponse {
Allowed = response.Allowed ?? false, Request = request, Error = null
});
}
catch (FgaApiAuthenticationError e) {
responses.Add(new BatchCheckSingleResponse {Allowed = false, Request = request, Error = e});
}
catch (FgaApiInternalError e) {
responses.Add(new BatchCheckSingleResponse {Allowed = false, Request = request, Error = e});
}
catch (FgaApiValidationError e) {
responses.Add(new BatchCheckSingleResponse {Allowed = false, Request = request, Error = e});
}
catch (FgaApiNotFoundError e) {
responses.Add(new BatchCheckSingleResponse {Allowed = false, Request = request, Error = e});
}
catch (FgaApiRateLimitExceededError e) {
responses.Add(new BatchCheckSingleResponse {Allowed = false, Request = request, Error = e});
}
catch (Exception e) {
responses.Add(new BatchCheckSingleResponse {Allowed = false, Request = request, Error = e});
}
Expand Down Expand Up @@ -368,7 +450,7 @@ public class {{appShortName}}Client : IDisposable {
* ListRelations - List all the relations a user has with an object (evaluates)
*/
public async Task<ListRelationsResponse> ListRelations(IClientListRelationsRequest body,
IClientBatchCheckOptions? options = default,
IClientListRelationsOptions? options = default,
CancellationToken cancellationToken = default) {
if (body.Relations.Count == 0) {
throw new FgaValidationError("At least 1 relation to check has to be provided when calling ListRelations");
Expand Down
64 changes: 64 additions & 0 deletions config/clients/dotnet/template/Client/ClientExtensions.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{{>partial_header}}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace {{packageName}}.Client
{
/// <summary>
/// Extensions methods for collections
/// </summary>
internal static class Extensions
{
/// <summary>
/// Splits a list into chunks of a specified size.
/// </summary>
/// <typeparam name="T">The type of the elements in the list.</typeparam>
/// <param name="source">The source list to chunk.</param>
/// <param name="chunkSize">The size of each chunk.</param>
/// <returns>An enumerable of chunks.</returns>
public static IEnumerable<List<T>> Chunk<T>(this List<T> source, int chunkSize)
{
for (int i = 0; i < source.Count; i += chunkSize)
{
yield return source.GetRange(i, Math.Min(chunkSize, source.Count - i));
}
}

/// <summary>
/// Asynchronously executes an action for each element in a collection.
/// </summary>
/// <typeparam name="T">The type of the elements in the source.</typeparam>
/// <param name="source">The source enumerable.</param>
/// <param name="action">The action to execute for each element.</param>
/// <param name="maxDegreeOfParallelism">The maximum degree of parallelism.</param>
/// <returns>A task that represents the completion of all parallel operations.</returns>
public static async Task ForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> action, int maxDegreeOfParallelism = 5)
{
var tasks = new List<Task>();
var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParallelism);

foreach (var item in source)
{
await throttler.WaitAsync();

tasks.Add(Task.Run(async () =>
{
try
{
await action(item);
}
finally
{
throttler.Release();
}
}));
}

await Task.WhenAll(tasks);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{{>partial_header}}

using System.Collections.Generic;

namespace {{packageName}}.Client
{
/// <summary>
/// Extensions for Dictionary
/// </summary>
public static class DictionaryExtensions
{
/// <summary>
/// Gets the value associated with the specified key if the key exists in the dictionary, or returns the default value.
/// </summary>
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
/// <param name="dictionary">The dictionary to search in.</param>
/// <param name="key">The key to find.</param>
/// <param name="defaultValue">The default value to return if the key is not found.</param>
/// <returns>The value associated with the key, or the default value if the key is not found.</returns>
public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue = default)
{
if (dictionary == null)
{
return defaultValue;
}

return dictionary.TryGetValue(key, out var value) ? value : defaultValue;
}
}
}
Loading
Loading