From 648d4d5aa686ed79ff04675781ee9b7279613c22 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 18:46:33 +0800 Subject: [PATCH 1/9] feat: expose ValueJsonConverter and add JsonSourceGenerator test cases Signed-off-by: Weihan Li --- src/OpenFeature/Model/ValueJsonConverter.cs | 7 +++++- test/OpenFeature.Tests/StructureTests.cs | 26 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature/Model/ValueJsonConverter.cs b/src/OpenFeature/Model/ValueJsonConverter.cs index 1551106c..911cc45f 100644 --- a/src/OpenFeature/Model/ValueJsonConverter.cs +++ b/src/OpenFeature/Model/ValueJsonConverter.cs @@ -4,11 +4,16 @@ namespace OpenFeature.Model; -internal sealed class ValueJsonConverter : JsonConverter +/// +/// A for for Json serialization +/// +public sealed class ValueJsonConverter : JsonConverter { + /// public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) => WriteJsonValue(value, writer); + /// public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => ReadJsonValue(ref reader); diff --git a/test/OpenFeature.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs index c7b6b878..b9a27ba7 100644 --- a/test/OpenFeature.Tests/StructureTests.cs +++ b/test/OpenFeature.Tests/StructureTests.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using OpenFeature.Model; namespace OpenFeature.Tests; @@ -125,6 +126,15 @@ public void JsonSerializeTest(Value value, string expectedJson) Assert.True(JsonNode.DeepEquals(expectJsonNode, serializedJsonNode)); } + [Theory] + [MemberData(nameof(JsonSerializeTestData))] + public void JsonSerializeWithGeneratorTest(Value value, string expectedJson) + { + var serializedJsonNode = JsonSerializer.SerializeToNode(value, ValueJsonSerializerContext.Default.Value); + var expectJsonNode = JsonNode.Parse(expectedJson); + Assert.True(JsonNode.DeepEquals(expectJsonNode, serializedJsonNode)); + } + [Theory] [MemberData(nameof(JsonSerializeTestData))] public void JsonDeserializeTest(Value value, string expectedJson) @@ -135,6 +145,17 @@ public void JsonDeserializeTest(Value value, string expectedJson) Assert.True(JsonNode.DeepEquals(expectJsonNode, serializedJsonNode)); } + [Theory] + [MemberData(nameof(JsonSerializeTestData))] + public void JsonDeserializeWithGeneratorTest(Value value, string expectedJson) + { + var serializedJsonNode = JsonSerializer.SerializeToNode(value, ValueJsonSerializerContext.Default.Value); + var expectValue = JsonSerializer.Deserialize(expectedJson, ValueJsonSerializerContext.Default.Value); + Assert.NotNull(expectValue); + var expectJsonNode = JsonSerializer.SerializeToNode(expectValue, ValueJsonSerializerContext.Default.Value); + Assert.True(JsonNode.DeepEquals(expectJsonNode, serializedJsonNode)); + } + public static IEnumerable JsonSerializeTestData() { yield return [new Value("test"), "\"test\""]; @@ -178,3 +199,8 @@ public static IEnumerable JsonSerializeTestData() ]; } } + +[JsonSerializable(typeof(Value))] +public partial class ValueJsonSerializerContext : JsonSerializerContext +{ +} From ff4e2161c71edc9b8bb1e3e6edff6df3fecec06f Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 18:54:52 +0800 Subject: [PATCH 2/9] style: apply dotnet-format Signed-off-by: Weihan Li --- test/OpenFeature.Tests/StructureTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs index b9a27ba7..9412e5d3 100644 --- a/test/OpenFeature.Tests/StructureTests.cs +++ b/test/OpenFeature.Tests/StructureTests.cs @@ -152,7 +152,7 @@ public void JsonDeserializeWithGeneratorTest(Value value, string expectedJson) var serializedJsonNode = JsonSerializer.SerializeToNode(value, ValueJsonSerializerContext.Default.Value); var expectValue = JsonSerializer.Deserialize(expectedJson, ValueJsonSerializerContext.Default.Value); Assert.NotNull(expectValue); - var expectJsonNode = JsonSerializer.SerializeToNode(expectValue, ValueJsonSerializerContext.Default.Value); + var expectJsonNode = JsonSerializer.SerializeToNode(expectValue, ValueJsonSerializerContext.Default.Value); Assert.True(JsonNode.DeepEquals(expectJsonNode, serializedJsonNode)); } From a82b99857e10c7227615ab505305cf00e7647507 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 19:10:35 +0800 Subject: [PATCH 3/9] feat: let the sample aot safe Signed-off-by: Weihan Li --- samples/AspNetCore/Program.cs | 34 +++++++++++++++++++- samples/AspNetCore/Samples.AspNetCore.csproj | 1 + 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs index e0921307..76c7996f 100644 --- a/samples/AspNetCore/Program.cs +++ b/samples/AspNetCore/Program.cs @@ -1,9 +1,11 @@ +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using OpenFeature; using OpenFeature.DependencyInjection.Providers.Memory; using OpenFeature.Hooks; +using OpenFeature.Model; using OpenFeature.Providers.Memory; -using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -11,6 +13,11 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + builder.Services.AddProblemDetails(); // Configure OpenTelemetry @@ -40,6 +47,13 @@ { "welcome-message", new Flag( new Dictionary { { "show", true }, { "hide", false } }, "show") + }, + { + "test-config", new Flag(new Dictionary() + { + { "enable", new TestConfig { Threshold = 100 } }, + { "disable", new TestConfig { Threshold = 0} } + }, "disable") } }); }); @@ -60,5 +74,23 @@ return TypedResults.Ok("Hello world!"); }); +app.MapGet("/test-config", async ([FromServices] IFeatureClient featureClient) => +{ + var testConfigValue = await featureClient.GetObjectValueAsync("test-config", + new Value(Structure.Builder().Set("Threshold", 0).Build()) + ); + var node = JsonSerializer.SerializeToNode(testConfigValue, AppJsonSerializerContext.Default.Value); + return Results.Ok(node); +}); app.Run(); + + +public class TestConfig +{ + public int Threshold { get; set; } = 10; +} + +[JsonSerializable(typeof(TestConfig))] +[JsonSerializable(typeof(Value))] +public partial class AppJsonSerializerContext : JsonSerializerContext; diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index cd249ab3..74460182 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -2,6 +2,7 @@ false + true From 2319320915e472c35ad745332e66ac6aa132d3b3 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 19:29:02 +0800 Subject: [PATCH 4/9] feat: enable aot analyzer and add necessary annotation Signed-off-by: Weihan Li --- src/Directory.Build.props | 4 +++ .../OpenFeatureBuilderExtensions.cs | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 992a6195..3b787904 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,3 +1,7 @@ + + + $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0')) + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 01a535e0..d676dc5e 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -272,7 +272,11 @@ public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, /// The instance. /// Optional factory for controlling how will be created in the DI container. /// The instance. - public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, Func? implementationFactory = null) + public static OpenFeatureBuilder AddHook< +#if NET + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + THook>(this OpenFeatureBuilder builder, Func? implementationFactory = null) where THook : Hook { return builder.AddHook(typeof(THook).Name, implementationFactory); @@ -285,7 +289,11 @@ public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, /// The instance. /// Instance of Hook to inject into the OpenFeature context. /// The instance. - public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, THook hook) + public static OpenFeatureBuilder AddHook< +#if NET + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + THook>(this OpenFeatureBuilder builder, THook hook) where THook : Hook { return builder.AddHook(typeof(THook).Name, hook); @@ -299,7 +307,11 @@ public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, /// The name of the that is being added. /// Instance of Hook to inject into the OpenFeature context. /// The instance. - public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, string hookName, THook hook) + public static OpenFeatureBuilder AddHook< +#if NET + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + THook>(this OpenFeatureBuilder builder, string hookName, THook hook) where THook : Hook { return builder.AddHook(hookName, _ => hook); @@ -313,7 +325,12 @@ public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, /// The name of the that is being added. /// Optional factory for controlling how will be created in the DI container. /// The instance. - public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, string hookName, Func? implementationFactory = null) + public static OpenFeatureBuilder AddHook< +#if NET + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + THook> + (this OpenFeatureBuilder builder, string hookName, Func? implementationFactory = null) where THook : Hook { builder.Services.PostConfigure(options => options.AddHookName(hookName)); From a93a669801e8308b32deba34f8c9dbd552df21ad Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 20:01:52 +0800 Subject: [PATCH 5/9] feat: update aot support for sample project Signed-off-by: Weihan Li --- .github/workflows/ci.yml | 5 +++++ samples/AspNetCore/Program.cs | 4 ++-- samples/AspNetCore/Samples.AspNetCore.csproj | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5b5edb7..46867e61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,11 @@ jobs: - name: Test run: dotnet test -c Release --no-build --logger GitHubActions + - name: aot-publish test + run: | + cd ./samples/AspNetCore + dotnet publish -p:PublishAot=true + packaging: needs: build diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs index 76c7996f..88aa0b0b 100644 --- a/samples/AspNetCore/Program.cs +++ b/samples/AspNetCore/Program.cs @@ -10,7 +10,7 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateSlimBuilder(args); // Add services to the container. builder.Services.ConfigureHttpJsonOptions(options => @@ -77,7 +77,7 @@ app.MapGet("/test-config", async ([FromServices] IFeatureClient featureClient) => { var testConfigValue = await featureClient.GetObjectValueAsync("test-config", - new Value(Structure.Builder().Set("Threshold", 0).Build()) + new Value(Structure.Builder().Set("Threshold", 0).Build()) ); var node = JsonSerializer.SerializeToNode(testConfigValue, AppJsonSerializerContext.Default.Value); return Results.Ok(node); diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index 74460182..ac0cd01c 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -2,7 +2,9 @@ false - true + + false + true From b9b1ab96fa1f5e60bfdcba1cecf25925edcb9372 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 20:12:23 +0800 Subject: [PATCH 6/9] build: fix aot publish error Signed-off-by: Weihan Li --- .github/workflows/ci.yml | 3 +-- samples/AspNetCore/Samples.AspNetCore.csproj | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46867e61..313d7e76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,8 +55,7 @@ jobs: - name: aot-publish test run: | - cd ./samples/AspNetCore - dotnet publish -p:PublishAot=true + dotnet publish ./samples/AspNetCore/Samples.AspNetCore.csproj -p:EnableAot=true packaging: needs: build diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index ac0cd01c..dcf50361 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -2,8 +2,9 @@ false - - false + + + true true From 03829ebf29fe8be4b9d4aa548e94cf59f5a2f14a Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 20:25:45 +0800 Subject: [PATCH 7/9] build: simplify the PublishAot error workaround Signed-off-by: Weihan Li --- .github/workflows/ci.yml | 2 +- .github/workflows/dotnet-format.yml | 4 +++- samples/AspNetCore/Samples.AspNetCore.csproj | 4 +--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 313d7e76..1f72a15f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: - name: aot-publish test run: | - dotnet publish ./samples/AspNetCore/Samples.AspNetCore.csproj -p:EnableAot=true + dotnet publish ./samples/AspNetCore/Samples.AspNetCore.csproj packaging: needs: build diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index e35e3775..5e572a92 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -23,4 +23,6 @@ jobs: global-json-file: global.json - name: dotnet format - run: dotnet format --verify-no-changes OpenFeature.slnx + run: | + # Exclude diagnostics to work around dotnet-format issue, see https://github.com/dotnet/sdk/issues/50012 + dotnet format --verify-no-changes --exclude-diagnostics=IL2026 --exclude-diagnostics=IL3050 OpenFeature.slnx diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index dcf50361..b6223bd0 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -2,9 +2,7 @@ false - - - true + true true From c0b8ff7604227dae3b7c1046830e614932542d4f Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 20:28:25 +0800 Subject: [PATCH 8/9] build: fix format action error Signed-off-by: Weihan Li --- .github/workflows/dotnet-format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 5e572a92..75f60375 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -25,4 +25,4 @@ jobs: - name: dotnet format run: | # Exclude diagnostics to work around dotnet-format issue, see https://github.com/dotnet/sdk/issues/50012 - dotnet format --verify-no-changes --exclude-diagnostics=IL2026 --exclude-diagnostics=IL3050 OpenFeature.slnx + dotnet format --verify-no-changes OpenFeature.slnx --exclude-diagnostics IL2026 --exclude-diagnostics IL3050 From f4257b859d24913187acbefe59c8d5250750f840 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 31 Jul 2025 21:20:53 +0800 Subject: [PATCH 9/9] sample: update sample usage Signed-off-by: Weihan Li --- samples/AspNetCore/Program.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs index 88aa0b0b..e8faf5a5 100644 --- a/samples/AspNetCore/Program.cs +++ b/samples/AspNetCore/Program.cs @@ -49,10 +49,11 @@ new Dictionary { { "show", true }, { "hide", false } }, "show") }, { - "test-config", new Flag(new Dictionary() + "test-config", new Flag(new Dictionary() { - { "enable", new TestConfig { Threshold = 100 } }, - { "disable", new TestConfig { Threshold = 0} } + { "enable", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 100).Build()) }, + { "half", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 50).Build()) }, + { "disable", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 0).Build()) } }, "disable") } }); @@ -77,10 +78,11 @@ app.MapGet("/test-config", async ([FromServices] IFeatureClient featureClient) => { var testConfigValue = await featureClient.GetObjectValueAsync("test-config", - new Value(Structure.Builder().Set("Threshold", 0).Build()) + new Value(Structure.Builder().Set("Threshold", 50).Build()) ); - var node = JsonSerializer.SerializeToNode(testConfigValue, AppJsonSerializerContext.Default.Value); - return Results.Ok(node); + var json = JsonSerializer.Serialize(testConfigValue, AppJsonSerializerContext.Default.Value); + var config = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.TestConfig); + return Results.Ok(config); }); app.Run();