From c6c700a777152f10004d8c4a1389cc3de079243c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 13:21:28 +0100
Subject: [PATCH 01/27] feat: enhance ValueJsonConverter for AOT compatibility
with manual JSON handling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
src/OpenFeature/Model/ValueJsonConverter.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/OpenFeature/Model/ValueJsonConverter.cs b/src/OpenFeature/Model/ValueJsonConverter.cs
index 911cc45f..7ffbf9c1 100644
--- a/src/OpenFeature/Model/ValueJsonConverter.cs
+++ b/src/OpenFeature/Model/ValueJsonConverter.cs
@@ -5,7 +5,9 @@
namespace OpenFeature.Model;
///
-/// A for for Json serialization
+/// A for for Json serialization.
+/// This converter is AOT-compatible as it uses manual JSON reading/writing
+/// instead of reflection-based serialization.
///
public sealed class ValueJsonConverter : JsonConverter
{
From bdb394bb153a4a91e5c3d802baac42521b2279b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 13:31:19 +0100
Subject: [PATCH 02/27] feat: refactor EnumExtensions to improve AOT
compatibility and remove reflection
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
src/OpenFeature/Constant/ErrorType.cs | 18 ++++++--------
src/OpenFeature/Extension/EnumExtensions.cs | 27 ++++++++++++++++++---
2 files changed, 31 insertions(+), 14 deletions(-)
diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs
index d36f3d96..3717bcad 100644
--- a/src/OpenFeature/Constant/ErrorType.cs
+++ b/src/OpenFeature/Constant/ErrorType.cs
@@ -1,5 +1,3 @@
-using System.ComponentModel;
-
namespace OpenFeature.Constant;
///
@@ -16,40 +14,40 @@ public enum ErrorType
///
/// Provider has yet been initialized
///
- [Description("PROVIDER_NOT_READY")] ProviderNotReady,
+ ProviderNotReady,
///
/// Provider was unable to find the flag
///
- [Description("FLAG_NOT_FOUND")] FlagNotFound,
+ FlagNotFound,
///
/// Provider failed to parse the flag response
///
- [Description("PARSE_ERROR")] ParseError,
+ ParseError,
///
/// Request type does not match the expected type
///
- [Description("TYPE_MISMATCH")] TypeMismatch,
+ TypeMismatch,
///
/// Abnormal execution of the provider
///
- [Description("GENERAL")] General,
+ General,
///
/// Context does not satisfy provider requirements.
///
- [Description("INVALID_CONTEXT")] InvalidContext,
+ InvalidContext,
///
/// Context does not contain a targeting key and the provider requires one.
///
- [Description("TARGETING_KEY_MISSING")] TargetingKeyMissing,
+ TargetingKeyMissing,
///
/// The provider has entered an irrecoverable error state.
///
- [Description("PROVIDER_FATAL")] ProviderFatal,
+ ProviderFatal,
}
diff --git a/src/OpenFeature/Extension/EnumExtensions.cs b/src/OpenFeature/Extension/EnumExtensions.cs
index 73c39125..be84ca3f 100644
--- a/src/OpenFeature/Extension/EnumExtensions.cs
+++ b/src/OpenFeature/Extension/EnumExtensions.cs
@@ -1,13 +1,32 @@
-using System.ComponentModel;
+using OpenFeature.Constant;
namespace OpenFeature.Extension;
internal static class EnumExtensions
{
+ ///
+ /// Gets the description of an enum value without using reflection.
+ /// This is AOT-compatible and only supports specific known enum types.
+ ///
+ /// The enum value to get the description for
+ /// The description string or the enum value as string if no description is available
public static string GetDescription(this Enum value)
{
- var field = value.GetType().GetField(value.ToString());
- var attribute = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
- return attribute?.Description ?? value.ToString();
+ return value switch
+ {
+ // ErrorType descriptions
+ ErrorType.None => "NONE",
+ ErrorType.ProviderNotReady => "PROVIDER_NOT_READY",
+ ErrorType.FlagNotFound => "FLAG_NOT_FOUND",
+ ErrorType.ParseError => "PARSE_ERROR",
+ ErrorType.TypeMismatch => "TYPE_MISMATCH",
+ ErrorType.General => "GENERAL",
+ ErrorType.InvalidContext => "INVALID_CONTEXT",
+ ErrorType.TargetingKeyMissing => "TARGETING_KEY_MISSING",
+ ErrorType.ProviderFatal => "PROVIDER_FATAL",
+
+ // Fallback for any other enum types
+ _ => value.ToString()
+ };
}
}
From c41cab71a998623c5f1404ef4617d14d62857c45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 13:37:39 +0100
Subject: [PATCH 03/27] feat: add OpenFeatureJsonSerializerContext for AOT
compilation support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../OpenFeatureJsonSerializerContext.cs | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 src/OpenFeature/Serialization/OpenFeatureJsonSerializerContext.cs
diff --git a/src/OpenFeature/Serialization/OpenFeatureJsonSerializerContext.cs b/src/OpenFeature/Serialization/OpenFeatureJsonSerializerContext.cs
new file mode 100644
index 00000000..820474cb
--- /dev/null
+++ b/src/OpenFeature/Serialization/OpenFeatureJsonSerializerContext.cs
@@ -0,0 +1,28 @@
+using System.Collections.Immutable;
+using System.Text.Json.Serialization;
+using OpenFeature.Model;
+
+namespace OpenFeature.Serialization;
+
+///
+/// JSON serializer context for AOT compilation support.
+/// This ensures that all necessary types are pre-compiled for JSON serialization
+/// when using NativeAOT.
+///
+[JsonSerializable(typeof(Value))]
+[JsonSerializable(typeof(Structure))]
+[JsonSerializable(typeof(EvaluationContext))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(ImmutableDictionary))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(ImmutableList))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(double))]
+[JsonSerializable(typeof(DateTime))]
+[JsonSourceGenerationOptions(
+ WriteIndented = false,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+public partial class OpenFeatureJsonSerializerContext : JsonSerializerContext;
From 56608c06685aea57c261b078b8bd9c3ee58ea8b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 13:39:58 +0100
Subject: [PATCH 04/27] feat: add AOT and trimming support for net8.0 and
net9.0
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
build/Common.prod.props | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 89451aca..4696b675 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -1,5 +1,5 @@
-
+
true
@@ -24,8 +24,15 @@
$(VersionNumber)
+
+
+ true
+ true
+ true
+
+
-
+
From ed3965284f1bc48ce5c5f308b72b14e1ada19a33 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 13:59:56 +0100
Subject: [PATCH 05/27] feat: add NativeAOT compatibility tests and project
configuration
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
Directory.Packages.props | 2 +
OpenFeature.slnx | 3 +-
src/OpenFeature/OpenFeature.csproj | 3 +-
.../OpenFeature.AotCompatibility.csproj | 34 ++++
test/OpenFeature.AotCompatibility/Program.cs | 187 ++++++++++++++++++
5 files changed, 227 insertions(+), 2 deletions(-)
create mode 100644 test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
create mode 100644 test/OpenFeature.AotCompatibility/Program.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index fe88537d..5f27a569 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -36,6 +36,8 @@
+
+
diff --git a/OpenFeature.slnx b/OpenFeature.slnx
index 0f445b44..fff54cff 100644
--- a/OpenFeature.slnx
+++ b/OpenFeature.slnx
@@ -64,7 +64,8 @@
+
-
\ No newline at end of file
+
diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj
index 243ab850..0344bdbc 100644
--- a/src/OpenFeature/OpenFeature.csproj
+++ b/src/OpenFeature/OpenFeature.csproj
@@ -21,7 +21,8 @@
+
-
\ No newline at end of file
+
diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
new file mode 100644
index 00000000..86c42720
--- /dev/null
+++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net9.0
+ Exe
+ enable
+ enable
+
+
+ true
+ true
+ true
+ full
+ Size
+
+
+ false
+ NU1903
+ OpenFeature.AotTests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenFeature.AotCompatibility/Program.cs b/test/OpenFeature.AotCompatibility/Program.cs
new file mode 100644
index 00000000..1a13b629
--- /dev/null
+++ b/test/OpenFeature.AotCompatibility/Program.cs
@@ -0,0 +1,187 @@
+using System.Text.Json;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenFeature.Constant;
+using OpenFeature.Extension;
+using OpenFeature.Model;
+using OpenFeature.Serialization;
+
+namespace OpenFeature.AotTests;
+
+///
+/// This program validates OpenFeature SDK compatibility with NativeAOT.
+/// It tests core functionality to ensure everything works correctly when compiled with AOT.
+///
+internal class Program
+{
+ private static async Task Main(string[] args)
+ {
+ Console.WriteLine("OpenFeature NativeAOT Compatibility Test");
+ Console.WriteLine("==========================================");
+
+ try
+ {
+ // Test basic API functionality
+ await TestBasicApiAsync();
+
+ // Test JSON serialization with AOT-compatible serializer context
+ TestJsonSerialization();
+
+ // Test dependency injection
+ await TestDependencyInjectionAsync();
+
+ // Test error handling and enum descriptions
+ TestErrorHandling();
+
+ Console.WriteLine("\nAll tests passed! OpenFeature is AOT-compatible.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"\nAOT compatibility test failed: {ex.Message}");
+ Console.WriteLine(ex.StackTrace);
+ Environment.Exit(1);
+ }
+ }
+
+ private static async Task TestBasicApiAsync()
+ {
+ Console.WriteLine("\nTesting basic API functionality...");
+
+ // Test singleton instance access
+ var api = Api.Instance;
+ Console.WriteLine($"✓- API instance created: {api.GetType().Name}");
+
+ // Test client creation
+ var client = api.GetClient("test-client", "1.0.0");
+ Console.WriteLine($"✓- Client created: {client.GetType().Name}");
+
+ // Test flag evaluation with default provider (NoOpProvider)
+ var boolResult = await client.GetBooleanValueAsync("test-flag", false);
+ Console.WriteLine($"✓- Boolean flag evaluation: {boolResult}");
+
+ var stringResult = await client.GetStringValueAsync("test-string-flag", "default");
+ Console.WriteLine($"✓- String flag evaluation: {stringResult}");
+
+ var intResult = await client.GetIntegerValueAsync("test-int-flag", 42);
+ Console.WriteLine($"✓- Integer flag evaluation: {intResult}");
+
+ var doubleResult = await client.GetDoubleValueAsync("test-double-flag", 3.14);
+ Console.WriteLine($"✓- Double flag evaluation: {doubleResult}");
+
+ // Test evaluation context
+ var context = EvaluationContext.Builder()
+ .Set("userId", "user123")
+ .Set("enabled", true)
+ .Build();
+ api.SetContext(context);
+ Console.WriteLine($"✓- Evaluation context set with {context.Count} attributes");
+ }
+
+ private static void TestJsonSerialization()
+ {
+ Console.WriteLine("\nTesting JSON serialization with AOT context...");
+
+ // Test Value serialization with AOT-compatible context
+ var structureBuilder = Structure.Builder()
+ .Set("name", "test")
+ .Set("enabled", true)
+ .Set("count", 42)
+ .Set("score", 98.5);
+
+ var structure = structureBuilder.Build();
+ var value = new Value(structure);
+
+ try
+ {
+ // Serialize using the AOT-compatible context
+ var json = JsonSerializer.Serialize(value, OpenFeatureJsonSerializerContext.Default.Value);
+ Console.WriteLine($"✓- Value serialized to JSON: {json}");
+
+ // Deserialize back
+ var deserializedValue = JsonSerializer.Deserialize(json, OpenFeatureJsonSerializerContext.Default.Value);
+ Console.WriteLine($"✓- Value deserialized from JSON successfully: {value}", deserializedValue);
+ }
+ catch (Exception ex)
+ {
+ // Fallback test with the custom converter (should still work)
+ Console.WriteLine($"X- AOT context serialization failed, testing fallback: {ex.Message}");
+ }
+ }
+
+ private static async Task TestDependencyInjectionAsync()
+ {
+ Console.WriteLine("\nTesting dependency injection...");
+
+ var builder = Host.CreateApplicationBuilder();
+
+ // Add OpenFeature with DI
+ builder.Services.AddOpenFeature(of => of.AddProvider(_ => new TestProvider()).AddHook(_ => new TestHook()));
+
+ builder.Services.AddLogging(logging => logging.AddConsole());
+
+ using var host = builder.Build();
+
+ var api = host.Services.GetRequiredService();
+ Console.WriteLine($"✓- FeatureClient resolved from DI: {api.GetType().Name}");
+
+ var result = await api.GetIntegerValueAsync("di-test-flag", 1);
+ Console.WriteLine($"✓- Flag evaluation via DI: {result}");
+ }
+
+ private static void TestErrorHandling()
+ {
+ Console.WriteLine("\nTesting error handling and enum descriptions...");
+
+ // Test ErrorType descriptions (this was the main reflection usage we fixed)
+ var errorTypes = new[]
+ {
+ ErrorType.None,
+ ErrorType.ProviderNotReady,
+ ErrorType.FlagNotFound,
+ ErrorType.ParseError,
+ ErrorType.TypeMismatch,
+ ErrorType.General,
+ ErrorType.InvalidContext,
+ ErrorType.TargetingKeyMissing,
+ ErrorType.ProviderFatal
+ };
+
+ foreach (var errorType in errorTypes)
+ {
+ var description = errorType.GetDescription();
+ Console.WriteLine($"✓- {errorType}: '{description}'");
+ }
+ }
+}
+
+///
+/// A simple test provider for validating DI functionality
+///
+internal class TestProvider : FeatureProvider
+{
+ public override Metadata GetMetadata() => new("test-provider");
+
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ => Task.FromResult(new ResolutionDetails(flagKey, true));
+
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ => Task.FromResult(new ResolutionDetails(flagKey, "test-value"));
+
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ => Task.FromResult(new ResolutionDetails(flagKey, 123));
+
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ => Task.FromResult(new ResolutionDetails(flagKey, 123.45));
+
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ => Task.FromResult(new ResolutionDetails(flagKey, new Value("test")));
+}
+
+///
+/// A simple test hook for validating DI functionality
+///
+internal class TestHook : Hook
+{
+ // No implementation needed for this test
+}
From 6f5390a7ae4a317ff1f95339c5b087999cd0bbdb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 14:37:46 +0100
Subject: [PATCH 06/27] feat: update project structure for AOT compatibility
and add MultiProvider tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
...OpenFeature.Providers.MultiProvider.csproj | 1 +
.../OpenFeature.AotCompatibility.csproj | 6 +-
test/OpenFeature.AotCompatibility/Program.cs | 108 +++++++++++++++---
3 files changed, 98 insertions(+), 17 deletions(-)
diff --git a/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj b/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj
index 000f223b..c2e57daf 100644
--- a/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj
+++ b/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj
@@ -9,6 +9,7 @@
+
diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
index 86c42720..8bdea03d 100644
--- a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
+++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
@@ -16,13 +16,13 @@
false
NU1903
- OpenFeature.AotTests
+ OpenFeature.AotCompatibility
-
+
+
diff --git a/test/OpenFeature.AotCompatibility/Program.cs b/test/OpenFeature.AotCompatibility/Program.cs
index 1a13b629..74692a31 100644
--- a/test/OpenFeature.AotCompatibility/Program.cs
+++ b/test/OpenFeature.AotCompatibility/Program.cs
@@ -5,9 +5,12 @@
using OpenFeature.Constant;
using OpenFeature.Extension;
using OpenFeature.Model;
+using OpenFeature.Providers.MultiProvider;
+using OpenFeature.Providers.MultiProvider.Models;
+using OpenFeature.Providers.MultiProvider.Strategies;
using OpenFeature.Serialization;
-namespace OpenFeature.AotTests;
+namespace OpenFeature.AotCompatibility;
///
/// This program validates OpenFeature SDK compatibility with NativeAOT.
@@ -25,6 +28,9 @@ private static async Task Main(string[] args)
// Test basic API functionality
await TestBasicApiAsync();
+ // Test MultiProvider AOT compatibility
+ await TestMultiProviderAotCompatibilityAsync();
+
// Test JSON serialization with AOT-compatible serializer context
TestJsonSerialization();
@@ -78,6 +84,81 @@ private static async Task TestBasicApiAsync()
Console.WriteLine($"✓- Evaluation context set with {context.Count} attributes");
}
+ private static async Task TestMultiProviderAotCompatibilityAsync()
+ {
+ Console.WriteLine("\nTesting MultiProvider AOT compatibility...");
+
+ // Create test providers for MultiProvider
+ var primaryProvider = new TestProvider();
+ var fallbackProvider = new TestProvider();
+
+ // Create provider entries for MultiProvider
+ var providerEntries = new List
+ {
+ new(primaryProvider, "primary"), new(fallbackProvider, "fallback")
+ };
+
+ // Test MultiProvider creation with FirstMatchStrategy (default)
+ var multiProvider = new MultiProvider(providerEntries);
+ Console.WriteLine($"✓- MultiProvider created with {providerEntries.Count} providers");
+
+ // Test MultiProvider metadata
+ var metadata = multiProvider.GetMetadata();
+ Console.WriteLine($"✓- MultiProvider metadata: {metadata.Name}");
+
+ await TestStrategy(providerEntries, new FirstMatchStrategy(), "FirstMatchStrategy");
+ await TestStrategy(providerEntries, new ComparisonStrategy(), "ComparisonStrategy");
+ await TestStrategy(providerEntries, new FirstSuccessfulStrategy(), "FirstSuccessfulStrategy");
+ }
+
+ private static async Task TestStrategy(List providerEntries, BaseEvaluationStrategy strategy, string strategyName)
+ {
+ // Test MultiProvider with strategy
+ var multiProvider = new MultiProvider(providerEntries, strategy);
+ Console.WriteLine($"✓- MultiProvider created with {strategyName}");
+
+ // Test all value types with MultiProvider
+ var evaluationContext = EvaluationContext.Builder()
+ .Set("userId", "aot-test-user")
+ .Set("environment", "test")
+ .Build();
+
+ // Test boolean evaluation
+ var boolResult = await multiProvider.ResolveBooleanValueAsync("test-bool-flag", false, evaluationContext);
+ Console.WriteLine($"✓- MultiProvider boolean evaluation: {boolResult.Value} (from {boolResult.Variant})");
+
+ // Test string evaluation
+ var stringResult =
+ await multiProvider.ResolveStringValueAsync("test-string-flag", "default", evaluationContext);
+ Console.WriteLine($"✓- MultiProvider string evaluation: {stringResult.Value} (from {stringResult.Variant})");
+
+ // Test integer evaluation
+ var intResult = await multiProvider.ResolveIntegerValueAsync("test-int-flag", 0, evaluationContext);
+ Console.WriteLine($"✓- MultiProvider integer evaluation: {intResult.Value} (from {intResult.Variant})");
+
+ // Test double evaluation
+ var doubleResult = await multiProvider.ResolveDoubleValueAsync("test-double-flag", 0.0, evaluationContext);
+ Console.WriteLine($"✓- MultiProvider double evaluation: {doubleResult.Value} (from {doubleResult.Variant})");
+
+ // Test structure evaluation
+ var structureResult =
+ await multiProvider.ResolveStructureValueAsync("test-structure-flag", new Value("default"),
+ evaluationContext);
+ Console.WriteLine(
+ $"✓- MultiProvider structure evaluation: {structureResult.Value} (from {structureResult.Variant})");
+
+ // Test MultiProvider lifecycle
+ await multiProvider.InitializeAsync(evaluationContext);
+ Console.WriteLine("✓- MultiProvider initialization completed");
+
+ await multiProvider.ShutdownAsync();
+ Console.WriteLine("✓- MultiProvider shutdown completed");
+
+ // Test MultiProvider disposal
+ await multiProvider.DisposeAsync();
+ Console.WriteLine("✓- MultiProvider disposal completed");
+ }
+
private static void TestJsonSerialization()
{
Console.WriteLine("\nTesting JSON serialization with AOT context...");
@@ -136,14 +217,8 @@ private static void TestErrorHandling()
// Test ErrorType descriptions (this was the main reflection usage we fixed)
var errorTypes = new[]
{
- ErrorType.None,
- ErrorType.ProviderNotReady,
- ErrorType.FlagNotFound,
- ErrorType.ParseError,
- ErrorType.TypeMismatch,
- ErrorType.General,
- ErrorType.InvalidContext,
- ErrorType.TargetingKeyMissing,
+ ErrorType.None, ErrorType.ProviderNotReady, ErrorType.FlagNotFound, ErrorType.ParseError,
+ ErrorType.TypeMismatch, ErrorType.General, ErrorType.InvalidContext, ErrorType.TargetingKeyMissing,
ErrorType.ProviderFatal
};
@@ -162,19 +237,24 @@ internal class TestProvider : FeatureProvider
{
public override Metadata GetMetadata() => new("test-provider");
- public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new ResolutionDetails(flagKey, true));
- public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new ResolutionDetails(flagKey, "test-value"));
- public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new ResolutionDetails(flagKey, 123));
- public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new ResolutionDetails(flagKey, 123.45));
- public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new ResolutionDetails(flagKey, new Value("test")));
}
From 5c794e4af1985bc7771627fba0ed5428f16db32b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 14:40:37 +0100
Subject: [PATCH 07/27] fix: remove unnecessary Type attribute project in
solution file
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
OpenFeature.slnx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/OpenFeature.slnx b/OpenFeature.slnx
index fff54cff..03efdf49 100644
--- a/OpenFeature.slnx
+++ b/OpenFeature.slnx
@@ -53,7 +53,7 @@
-
+
@@ -64,8 +64,8 @@
-
-
+
+
From 5cd179e817cf47eed9217b4984038a20f57d07b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 15:10:28 +0100
Subject: [PATCH 08/27] feat: add unit tests for EnumExtensions.GetDescription
method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
test/OpenFeature.Tests/EnumExtensionsTests.cs | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 test/OpenFeature.Tests/EnumExtensionsTests.cs
diff --git a/test/OpenFeature.Tests/EnumExtensionsTests.cs b/test/OpenFeature.Tests/EnumExtensionsTests.cs
new file mode 100644
index 00000000..35e61a2e
--- /dev/null
+++ b/test/OpenFeature.Tests/EnumExtensionsTests.cs
@@ -0,0 +1,26 @@
+using OpenFeature.Constant;
+using OpenFeature.Extension;
+
+namespace OpenFeature.Tests;
+
+public class EnumExtensionsTests
+{
+ [Theory]
+ [InlineData(ErrorType.None, "NONE")]
+ [InlineData(ErrorType.ProviderNotReady, "PROVIDER_NOT_READY")]
+ [InlineData(ErrorType.FlagNotFound, "FLAG_NOT_FOUND")]
+ [InlineData(ErrorType.ParseError, "PARSE_ERROR")]
+ [InlineData(ErrorType.TypeMismatch, "TYPE_MISMATCH")]
+ [InlineData(ErrorType.General, "GENERAL")]
+ [InlineData(ErrorType.InvalidContext, "INVALID_CONTEXT")]
+ [InlineData(ErrorType.TargetingKeyMissing, "TARGETING_KEY_MISSING")]
+ [InlineData(ErrorType.ProviderFatal, "PROVIDER_FATAL")]
+ public void GetDescription_WithErrorType_ReturnsExpectedDescription(ErrorType errorType, string expectedDescription)
+ {
+ // Act
+ var result = errorType.GetDescription();
+
+ // Assert
+ Assert.Equal(expectedDescription, result);
+ }
+}
From 1c54a7fb4aea5f853f74e358c31e31c7bdb6ae5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 15:16:13 +0100
Subject: [PATCH 09/27] fix: remove trimming support properties for net8.0 and
net9.0
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
build/Common.prod.props | 2 --
1 file changed, 2 deletions(-)
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 4696b675..199961c6 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -27,8 +27,6 @@
true
- true
- true
From f62a88db7d9701ff8aa095272c41fe2487cb77fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 15:23:27 +0100
Subject: [PATCH 10/27] feat: add AOT compatibility workflow with
cross-platform testing and size comparison
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 241 ++++++++++++++++++++++++
1 file changed, 241 insertions(+)
create mode 100644 .github/workflows/aot-compatibility.yml
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
new file mode 100644
index 00000000..ac7ddb65
--- /dev/null
+++ b/.github/workflows/aot-compatibility.yml
@@ -0,0 +1,241 @@
+name: AOT Compatibility
+
+on:
+ push:
+ branches: [main, askpt/440-feature-investigate-nativeaot]
+ pull_request:
+ branches: [main]
+ merge_group:
+ workflow_dispatch:
+
+jobs:
+ aot-compatibility:
+ name: AOT Test (${{ matrix.os }}, ${{ matrix.arch }})
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Linux x64
+ - os: ubuntu-latest
+ arch: x64
+ runtime: linux-x64
+ # Linux ARM64
+ - os: ubuntu-24.04-arm
+ arch: arm64
+ runtime: linux-arm64
+ # Windows x64
+ - os: windows-latest
+ arch: x64
+ runtime: win-x64
+ # Windows ARM64
+ - os: windows-11-arm
+ arch: arm64
+ runtime: win-arm64
+ # macOS x64
+ - os: macos-13
+ arch: x64
+ runtime: osx-x64
+ # macOS ARM64 (Apple Silicon)
+ - os: macos-latest
+ arch: arm64
+ runtime: osx-arm64
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ global-json-file: global.json
+ source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ matrix.arch }}-nuget-
+ ${{ runner.os }}-nuget-
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build solution
+ run: dotnet build -c Release --no-restore
+
+ - name: Test AOT compatibility project build
+ run: dotnet build test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj -c Release --no-restore
+
+ - name: Publish AOT compatibility test (cross-platform)
+ if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
+ run: |
+ dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
+ -c Release \
+ -r ${{ matrix.runtime }} \
+ --self-contained true \
+ -p:PublishAot=true \
+ -p:IsAotCompatible=true \
+ -p:InvariantGlobalization=true \
+ -p:TrimMode=full \
+ -o ./aot-output
+
+ - name: Run AOT compatibility test
+ if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
+ shell: bash
+ run: |
+ if [[ "${{ runner.os }}" == "Windows" ]]; then
+ ./aot-output/OpenFeature.AotCompatibility.exe
+ else
+ chmod +x ./aot-output/OpenFeature.AotCompatibility
+ ./aot-output/OpenFeature.AotCompatibility
+ fi
+
+ # For ARM64 cross-compilation on non-ARM64 runners, we only test the build
+ - name: Test AOT cross-compilation build only (ARM64)
+ if: matrix.arch == 'arm64' && runner.arch != 'ARM64'
+ run: |
+ dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
+ -c Release \
+ -r ${{ matrix.runtime }} \
+ --self-contained true \
+ -p:PublishAot=true \
+ -p:IsAotCompatible=true \
+ -p:InvariantGlobalization=true \
+ -p:TrimMode=full \
+ -o ./aot-output-cross
+
+ - name: Test AspNetCore sample AOT compilation
+ if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
+ run: |
+ dotnet publish samples/AspNetCore/Samples.AspNetCore.csproj \
+ -c Release \
+ -r ${{ matrix.runtime }} \
+ --self-contained true \
+ -p:PublishAot=true \
+ -o ./aspnetcore-aot-output
+
+ - name: Verify AOT output size and characteristics
+ if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
+ shell: bash
+ run: |
+ echo "=== AOT Compatibility Test Binary ==="
+ if [[ "${{ runner.os }}" == "Windows" ]]; then
+ ls -la ./aot-output/OpenFeature.AotCompatibility.exe
+ echo "Binary size: $(stat -c %s ./aot-output/OpenFeature.AotCompatibility.exe) bytes"
+ else
+ ls -la ./aot-output/OpenFeature.AotCompatibility
+ echo "Binary size: $(stat -c %s ./aot-output/OpenFeature.AotCompatibility) bytes"
+ fi
+
+ echo ""
+ echo "=== AspNetCore Sample Binary ==="
+ if [[ "${{ runner.os }}" == "Windows" ]]; then
+ ls -la ./aspnetcore-aot-output/Samples.AspNetCore.exe
+ echo "Binary size: $(stat -c %s ./aspnetcore-aot-output/Samples.AspNetCore.exe) bytes"
+ else
+ ls -la ./aspnetcore-aot-output/Samples.AspNetCore
+ echo "Binary size: $(stat -c %s ./aspnetcore-aot-output/Samples.AspNetCore) bytes"
+ fi
+
+ - name: Upload AOT artifacts
+ if: always() && (matrix.arch != 'arm64' || runner.arch == 'ARM64')
+ uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
+ with:
+ name: aot-binaries-${{ matrix.os }}-${{ matrix.arch }}
+ path: |
+ aot-output/
+ aspnetcore-aot-output/
+ retention-days: 7
+
+ aot-size-comparison:
+ name: AOT Size Comparison
+ needs: aot-compatibility
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+
+ steps:
+ - name: Download all AOT artifacts
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
+ with:
+ pattern: aot-binaries-*
+ path: ./artifacts
+
+ - name: Generate size comparison report
+ shell: bash
+ run: |
+ echo "# AOT Binary Size Report" > size_report.md
+ echo "" >> size_report.md
+ echo "| Platform | Architecture | AOT Test Binary | AspNetCore Sample |" >> size_report.md
+ echo "|----------|--------------|-----------------|-------------------|" >> size_report.md
+
+ for dir in ./artifacts/aot-binaries-*; do
+ if [[ -d "$dir" ]]; then
+ platform=$(basename "$dir" | sed 's/aot-binaries-//' | sed 's/-[^-]*$//')
+ arch=$(basename "$dir" | sed 's/.*-//')
+
+ # Find the AOT test binary
+ if [[ -f "$dir/aot-output/OpenFeature.AotCompatibility.exe" ]]; then
+ aot_size=$(stat -c %s "$dir/aot-output/OpenFeature.AotCompatibility.exe")
+ elif [[ -f "$dir/aot-output/OpenFeature.AotCompatibility" ]]; then
+ aot_size=$(stat -c %s "$dir/aot-output/OpenFeature.AotCompatibility")
+ else
+ aot_size="N/A"
+ fi
+
+ # Find the AspNetCore sample binary
+ if [[ -f "$dir/aspnetcore-aot-output/Samples.AspNetCore.exe" ]]; then
+ aspnet_size=$(stat -c %s "$dir/aspnetcore-aot-output/Samples.AspNetCore.exe")
+ elif [[ -f "$dir/aspnetcore-aot-output/Samples.AspNetCore" ]]; then
+ aspnet_size=$(stat -c %s "$dir/aspnetcore-aot-output/Samples.AspNetCore")
+ else
+ aspnet_size="N/A"
+ fi
+
+ # Format sizes
+ if [[ "$aot_size" != "N/A" ]]; then
+ aot_size_mb=$(echo "scale=2; $aot_size / 1048576" | bc)
+ aot_display="${aot_size_mb}MB"
+ else
+ aot_display="N/A"
+ fi
+
+ if [[ "$aspnet_size" != "N/A" ]]; then
+ aspnet_size_mb=$(echo "scale=2; $aspnet_size / 1048576" | bc)
+ aspnet_display="${aspnet_size_mb}MB"
+ else
+ aspnet_display="N/A"
+ fi
+
+ echo "| $platform | $arch | $aot_display | $aspnet_display |" >> size_report.md
+ fi
+ done
+
+ echo "" >> size_report.md
+ echo "Generated on: $(date)" >> size_report.md
+
+ cat size_report.md
+
+ - name: Find PR Comment
+ uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
+ id: find-comment
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-author: 'github-actions[bot]'
+ body-includes: 'AOT Binary Size Report'
+
+ - name: Create or update PR comment
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
+ with:
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ issue-number: ${{ github.event.pull_request.number }}
+ body-path: size_report.md
+ edit-mode: replace
From f8f90fcfb313fa9b96dc0fc837441bcbdb0ecb9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 15 Aug 2025 15:30:58 +0100
Subject: [PATCH 11/27] fix: simplify AOT compatibility workflow by removing
unnecessary properties and AspNetCore sample tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 46 ++-----------------------
1 file changed, 3 insertions(+), 43 deletions(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index ac7ddb65..5a4f91ef 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -2,7 +2,7 @@ name: AOT Compatibility
on:
push:
- branches: [main, askpt/440-feature-investigate-nativeaot]
+ branches: [main]
pull_request:
branches: [main]
merge_group:
@@ -80,12 +80,7 @@ jobs:
run: |
dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
-c Release \
- -r ${{ matrix.runtime }} \
- --self-contained true \
- -p:PublishAot=true \
- -p:IsAotCompatible=true \
- -p:InvariantGlobalization=true \
- -p:TrimMode=full \
+ -r ${{ matrix.runtime }}
-o ./aot-output
- name: Run AOT compatibility test
@@ -106,23 +101,8 @@ jobs:
dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
-c Release \
-r ${{ matrix.runtime }} \
- --self-contained true \
- -p:PublishAot=true \
- -p:IsAotCompatible=true \
- -p:InvariantGlobalization=true \
- -p:TrimMode=full \
-o ./aot-output-cross
- - name: Test AspNetCore sample AOT compilation
- if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
- run: |
- dotnet publish samples/AspNetCore/Samples.AspNetCore.csproj \
- -c Release \
- -r ${{ matrix.runtime }} \
- --self-contained true \
- -p:PublishAot=true \
- -o ./aspnetcore-aot-output
-
- name: Verify AOT output size and characteristics
if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
shell: bash
@@ -136,24 +116,13 @@ jobs:
echo "Binary size: $(stat -c %s ./aot-output/OpenFeature.AotCompatibility) bytes"
fi
- echo ""
- echo "=== AspNetCore Sample Binary ==="
- if [[ "${{ runner.os }}" == "Windows" ]]; then
- ls -la ./aspnetcore-aot-output/Samples.AspNetCore.exe
- echo "Binary size: $(stat -c %s ./aspnetcore-aot-output/Samples.AspNetCore.exe) bytes"
- else
- ls -la ./aspnetcore-aot-output/Samples.AspNetCore
- echo "Binary size: $(stat -c %s ./aspnetcore-aot-output/Samples.AspNetCore) bytes"
- fi
-
- name: Upload AOT artifacts
if: always() && (matrix.arch != 'arm64' || runner.arch == 'ARM64')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: aot-binaries-${{ matrix.os }}-${{ matrix.arch }}
path: |
- aot-output/
- aspnetcore-aot-output/
+ aot-output
retention-days: 7
aot-size-comparison:
@@ -191,15 +160,6 @@ jobs:
aot_size="N/A"
fi
- # Find the AspNetCore sample binary
- if [[ -f "$dir/aspnetcore-aot-output/Samples.AspNetCore.exe" ]]; then
- aspnet_size=$(stat -c %s "$dir/aspnetcore-aot-output/Samples.AspNetCore.exe")
- elif [[ -f "$dir/aspnetcore-aot-output/Samples.AspNetCore" ]]; then
- aspnet_size=$(stat -c %s "$dir/aspnetcore-aot-output/Samples.AspNetCore")
- else
- aspnet_size="N/A"
- fi
-
# Format sizes
if [[ "$aot_size" != "N/A" ]]; then
aot_size_mb=$(echo "scale=2; $aot_size / 1048576" | bc)
From df63171be11e110abf0498d457d9585a2a2f5178 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Sat, 16 Aug 2025 07:13:28 +0100
Subject: [PATCH 12/27] fix: update AOT compatibility workflow to include
runtime in publish step and standardize comment formatting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index 5a4f91ef..15f6f487 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -80,7 +80,7 @@ jobs:
run: |
dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
-c Release \
- -r ${{ matrix.runtime }}
+ -r ${{ matrix.runtime }} \
-o ./aot-output
- name: Run AOT compatibility test
@@ -189,8 +189,8 @@ jobs:
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
- comment-author: 'github-actions[bot]'
- body-includes: 'AOT Binary Size Report'
+ comment-author: "github-actions[bot]"
+ body-includes: "AOT Binary Size Report"
- name: Create or update PR comment
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
From ea053a375bc83119fb8ebe02faa92abd8292dc45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Sat, 16 Aug 2025 07:21:14 +0100
Subject: [PATCH 13/27] fix: update AOT compatibility workflow to streamline
ARM64 handling and switch to PowerShell for script execution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 106 +++++++++---------------
1 file changed, 39 insertions(+), 67 deletions(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index 15f6f487..198b3896 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -76,7 +76,6 @@ jobs:
run: dotnet build test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj -c Release --no-restore
- name: Publish AOT compatibility test (cross-platform)
- if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
run: |
dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
-c Release \
@@ -84,40 +83,16 @@ jobs:
-o ./aot-output
- name: Run AOT compatibility test
- if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
- shell: bash
+ shell: pwsh
run: |
- if [[ "${{ runner.os }}" == "Windows" ]]; then
+ if ("${{ runner.os }}" -eq "Windows") {
./aot-output/OpenFeature.AotCompatibility.exe
- else
+ } else {
chmod +x ./aot-output/OpenFeature.AotCompatibility
./aot-output/OpenFeature.AotCompatibility
- fi
-
- # For ARM64 cross-compilation on non-ARM64 runners, we only test the build
- - name: Test AOT cross-compilation build only (ARM64)
- if: matrix.arch == 'arm64' && runner.arch != 'ARM64'
- run: |
- dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
- -c Release \
- -r ${{ matrix.runtime }} \
- -o ./aot-output-cross
-
- - name: Verify AOT output size and characteristics
- if: matrix.arch != 'arm64' || runner.arch == 'ARM64'
- shell: bash
- run: |
- echo "=== AOT Compatibility Test Binary ==="
- if [[ "${{ runner.os }}" == "Windows" ]]; then
- ls -la ./aot-output/OpenFeature.AotCompatibility.exe
- echo "Binary size: $(stat -c %s ./aot-output/OpenFeature.AotCompatibility.exe) bytes"
- else
- ls -la ./aot-output/OpenFeature.AotCompatibility
- echo "Binary size: $(stat -c %s ./aot-output/OpenFeature.AotCompatibility) bytes"
- fi
+ }
- name: Upload AOT artifacts
- if: always() && (matrix.arch != 'arm64' || runner.arch == 'ARM64')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: aot-binaries-${{ matrix.os }}-${{ matrix.arch }}
@@ -139,50 +114,47 @@ jobs:
path: ./artifacts
- name: Generate size comparison report
- shell: bash
+ shell: pwsh
run: |
- echo "# AOT Binary Size Report" > size_report.md
- echo "" >> size_report.md
- echo "| Platform | Architecture | AOT Test Binary | AspNetCore Sample |" >> size_report.md
- echo "|----------|--------------|-----------------|-------------------|" >> size_report.md
+ "# AOT Binary Size Report" | Out-File -FilePath size_report.md -Encoding utf8
+ "" | Out-File -FilePath size_report.md -Append -Encoding utf8
+ "| Platform | Architecture | AOT Test Binary | AspNetCore Sample |" | Out-File -FilePath size_report.md -Append -Encoding utf8
+ "|----------|--------------|-----------------|-------------------|" | Out-File -FilePath size_report.md -Append -Encoding utf8
+
+ $artifactDirs = Get-ChildItem -Path ./artifacts -Directory -Name "aot-binaries-*"
- for dir in ./artifacts/aot-binaries-*; do
- if [[ -d "$dir" ]]; then
- platform=$(basename "$dir" | sed 's/aot-binaries-//' | sed 's/-[^-]*$//')
- arch=$(basename "$dir" | sed 's/.*-//')
+ foreach ($dir in $artifactDirs) {
+ $fullPath = "./artifacts/$dir"
+ if (Test-Path -Path $fullPath -PathType Container) {
+ $platform = ($dir -replace '^aot-binaries-', '') -replace '-[^-]*$', ''
+ $arch = ($dir -split '-')[-1]
# Find the AOT test binary
- if [[ -f "$dir/aot-output/OpenFeature.AotCompatibility.exe" ]]; then
- aot_size=$(stat -c %s "$dir/aot-output/OpenFeature.AotCompatibility.exe")
- elif [[ -f "$dir/aot-output/OpenFeature.AotCompatibility" ]]; then
- aot_size=$(stat -c %s "$dir/aot-output/OpenFeature.AotCompatibility")
- else
- aot_size="N/A"
- fi
+ $aotSize = "N/A"
+ if (Test-Path "$fullPath/aot-output/OpenFeature.AotCompatibility.exe") {
+ $aotSize = (Get-Item "$fullPath/aot-output/OpenFeature.AotCompatibility.exe").Length
+ } elseif (Test-Path "$fullPath/aot-output/OpenFeature.AotCompatibility") {
+ $aotSize = (Get-Item "$fullPath/aot-output/OpenFeature.AotCompatibility").Length
+ }
# Format sizes
- if [[ "$aot_size" != "N/A" ]]; then
- aot_size_mb=$(echo "scale=2; $aot_size / 1048576" | bc)
- aot_display="${aot_size_mb}MB"
- else
- aot_display="N/A"
- fi
-
- if [[ "$aspnet_size" != "N/A" ]]; then
- aspnet_size_mb=$(echo "scale=2; $aspnet_size / 1048576" | bc)
- aspnet_display="${aspnet_size_mb}MB"
- else
- aspnet_display="N/A"
- fi
-
- echo "| $platform | $arch | $aot_display | $aspnet_display |" >> size_report.md
- fi
- done
-
- echo "" >> size_report.md
- echo "Generated on: $(date)" >> size_report.md
-
- cat size_report.md
+ if ($aotSize -ne "N/A") {
+ $aotSizeMb = [math]::Round($aotSize / 1048576, 2)
+ $aotDisplay = "$aotSizeMb" + "MB"
+ } else {
+ $aotDisplay = "N/A"
+ }
+
+ $aspnetDisplay = "N/A" # AspNetCore sample not implemented yet
+
+ "| $platform | $arch | $aotDisplay | $aspnetDisplay |" | Out-File -FilePath size_report.md -Append -Encoding utf8
+ }
+ }
+
+ "" | Out-File -FilePath size_report.md -Append -Encoding utf8
+ "Generated on: $(Get-Date)" | Out-File -FilePath size_report.md -Append -Encoding utf8
+
+ Get-Content size_report.md
- name: Find PR Comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
From 4cc832e7c39f2654fd58b910bfe5ba770c9a763d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Sat, 16 Aug 2025 07:25:49 +0100
Subject: [PATCH 14/27] fix: standardize shell usage and update publish command
syntax in AOT compatibility workflow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index 198b3896..c5c3ed29 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -67,19 +67,23 @@ jobs:
${{ runner.os }}-nuget-
- name: Restore dependencies
+ shell: pwsh
run: dotnet restore
- name: Build solution
+ shell: pwsh
run: dotnet build -c Release --no-restore
- name: Test AOT compatibility project build
+ shell: pwsh
run: dotnet build test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj -c Release --no-restore
- name: Publish AOT compatibility test (cross-platform)
+ shell: pwsh
run: |
- dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj \
- -c Release \
- -r ${{ matrix.runtime }} \
+ dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj `
+ -c Release `
+ -r ${{ matrix.runtime }} `
-o ./aot-output
- name: Run AOT compatibility test
From f462b4e49d21d2601341afde14a1edf17640702e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Sat, 16 Aug 2025 07:33:21 +0100
Subject: [PATCH 15/27] fix: update AOT size comparison report to remove
AspNetCore sample column and enhance binary size logging
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 25 +++++++++++++++----------
1 file changed, 15 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index c5c3ed29..123f9d05 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -122,8 +122,8 @@ jobs:
run: |
"# AOT Binary Size Report" | Out-File -FilePath size_report.md -Encoding utf8
"" | Out-File -FilePath size_report.md -Append -Encoding utf8
- "| Platform | Architecture | AOT Test Binary | AspNetCore Sample |" | Out-File -FilePath size_report.md -Append -Encoding utf8
- "|----------|--------------|-----------------|-------------------|" | Out-File -FilePath size_report.md -Append -Encoding utf8
+ "| Platform | Architecture | AOT Test Binary |" | Out-File -FilePath size_report.md -Append -Encoding utf8
+ "|----------|--------------|-----------------|" | Out-File -FilePath size_report.md -Append -Encoding utf8
$artifactDirs = Get-ChildItem -Path ./artifacts -Directory -Name "aot-binaries-*"
@@ -135,23 +135,28 @@ jobs:
# Find the AOT test binary
$aotSize = "N/A"
- if (Test-Path "$fullPath/aot-output/OpenFeature.AotCompatibility.exe") {
- $aotSize = (Get-Item "$fullPath/aot-output/OpenFeature.AotCompatibility.exe").Length
- } elseif (Test-Path "$fullPath/aot-output/OpenFeature.AotCompatibility") {
- $aotSize = (Get-Item "$fullPath/aot-output/OpenFeature.AotCompatibility").Length
+ $exePath = "$fullPath/aot-output/OpenFeature.AotCompatibility.exe"
+ $linuxPath = "$fullPath/aot-output/OpenFeature.AotCompatibility"
+
+ if (Test-Path $exePath) {
+ $aotSize = (Get-Item $exePath).Length
+ Write-Host "Found Windows binary: $exePath, Size: $aotSize bytes"
+ } elseif (Test-Path $linuxPath) {
+ $aotSize = (Get-Item $linuxPath).Length
+ Write-Host "Found Unix binary: $linuxPath, Size: $aotSize bytes"
+ } else {
+ Write-Host "No binary found in $fullPath/aot-output/"
}
# Format sizes
if ($aotSize -ne "N/A") {
$aotSizeMb = [math]::Round($aotSize / 1048576, 2)
- $aotDisplay = "$aotSizeMb" + "MB"
+ $aotDisplay = "{0:N2}MB" -f $aotSizeMb
} else {
$aotDisplay = "N/A"
}
- $aspnetDisplay = "N/A" # AspNetCore sample not implemented yet
-
- "| $platform | $arch | $aotDisplay | $aspnetDisplay |" | Out-File -FilePath size_report.md -Append -Encoding utf8
+ "| $platform | $arch | $aotDisplay |" | Out-File -FilePath size_report.md -Append -Encoding utf8
}
}
From 71c2bc4be1102d087a704e8281235a4e461bc2ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Mon, 18 Aug 2025 16:55:42 +0100
Subject: [PATCH 16/27] fix: remove AOT size comparison job and artifact upload
steps from workflow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 85 -------------------------
1 file changed, 85 deletions(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index 123f9d05..6e12bebf 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -95,88 +95,3 @@ jobs:
chmod +x ./aot-output/OpenFeature.AotCompatibility
./aot-output/OpenFeature.AotCompatibility
}
-
- - name: Upload AOT artifacts
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
- with:
- name: aot-binaries-${{ matrix.os }}-${{ matrix.arch }}
- path: |
- aot-output
- retention-days: 7
-
- aot-size-comparison:
- name: AOT Size Comparison
- needs: aot-compatibility
- runs-on: ubuntu-latest
- if: github.event_name == 'pull_request'
-
- steps:
- - name: Download all AOT artifacts
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
- with:
- pattern: aot-binaries-*
- path: ./artifacts
-
- - name: Generate size comparison report
- shell: pwsh
- run: |
- "# AOT Binary Size Report" | Out-File -FilePath size_report.md -Encoding utf8
- "" | Out-File -FilePath size_report.md -Append -Encoding utf8
- "| Platform | Architecture | AOT Test Binary |" | Out-File -FilePath size_report.md -Append -Encoding utf8
- "|----------|--------------|-----------------|" | Out-File -FilePath size_report.md -Append -Encoding utf8
-
- $artifactDirs = Get-ChildItem -Path ./artifacts -Directory -Name "aot-binaries-*"
-
- foreach ($dir in $artifactDirs) {
- $fullPath = "./artifacts/$dir"
- if (Test-Path -Path $fullPath -PathType Container) {
- $platform = ($dir -replace '^aot-binaries-', '') -replace '-[^-]*$', ''
- $arch = ($dir -split '-')[-1]
-
- # Find the AOT test binary
- $aotSize = "N/A"
- $exePath = "$fullPath/aot-output/OpenFeature.AotCompatibility.exe"
- $linuxPath = "$fullPath/aot-output/OpenFeature.AotCompatibility"
-
- if (Test-Path $exePath) {
- $aotSize = (Get-Item $exePath).Length
- Write-Host "Found Windows binary: $exePath, Size: $aotSize bytes"
- } elseif (Test-Path $linuxPath) {
- $aotSize = (Get-Item $linuxPath).Length
- Write-Host "Found Unix binary: $linuxPath, Size: $aotSize bytes"
- } else {
- Write-Host "No binary found in $fullPath/aot-output/"
- }
-
- # Format sizes
- if ($aotSize -ne "N/A") {
- $aotSizeMb = [math]::Round($aotSize / 1048576, 2)
- $aotDisplay = "{0:N2}MB" -f $aotSizeMb
- } else {
- $aotDisplay = "N/A"
- }
-
- "| $platform | $arch | $aotDisplay |" | Out-File -FilePath size_report.md -Append -Encoding utf8
- }
- }
-
- "" | Out-File -FilePath size_report.md -Append -Encoding utf8
- "Generated on: $(Get-Date)" | Out-File -FilePath size_report.md -Append -Encoding utf8
-
- Get-Content size_report.md
-
- - name: Find PR Comment
- uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
- id: find-comment
- with:
- issue-number: ${{ github.event.pull_request.number }}
- comment-author: "github-actions[bot]"
- body-includes: "AOT Binary Size Report"
-
- - name: Create or update PR comment
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
- with:
- comment-id: ${{ steps.find-comment.outputs.comment-id }}
- issue-number: ${{ github.event.pull_request.number }}
- body-path: size_report.md
- edit-mode: replace
From 2895325a0c2f2da526475d173b85485d27869d47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Mon, 18 Aug 2025 17:02:56 +0100
Subject: [PATCH 17/27] fix: update AOT compatibility workflow permissions and
enhance documentation for NativeAOT support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 5 +-
README.md | 4 +
docs/AOT_COMPATIBILITY.md | 182 ++++++++++++++++++++++++
3 files changed, 188 insertions(+), 3 deletions(-)
create mode 100644 docs/AOT_COMPATIBILITY.md
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index 6e12bebf..f8ff3fdc 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -11,6 +11,8 @@ on:
jobs:
aot-compatibility:
name: AOT Test (${{ matrix.os }}, ${{ matrix.arch }})
+ permissions:
+ contents: read
strategy:
fail-fast: false
matrix:
@@ -51,11 +53,8 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
global-json-file: global.json
- source-url: https://nuget.pkg.github.com/open-feature/index.json
- name: Cache NuGet packages
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
diff --git a/README.md b/README.md
index 2da256cd..44af242c 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,10 @@
Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5
+### NativeAOT Support
+
+✅ **Full NativeAOT Compatibility** - The OpenFeature .NET SDK is fully compatible with .NET NativeAOT compilation for fast startup and small deployment size. See the [AOT Compatibility Guide](docs/AOT_COMPATIBILITY.md) for detailed instructions.
+
### Install
Use the following to initialize your project:
diff --git a/docs/AOT_COMPATIBILITY.md b/docs/AOT_COMPATIBILITY.md
new file mode 100644
index 00000000..d242ae9b
--- /dev/null
+++ b/docs/AOT_COMPATIBILITY.md
@@ -0,0 +1,182 @@
+# OpenFeature .NET SDK - NativeAOT Compatibility
+
+The OpenFeature .NET SDK is compatible with .NET NativeAOT compilation, allowing you to create self-contained, native executables with faster startup times and lower memory usage.
+
+## Compatibility Status
+
+✅ **Fully Compatible** - The SDK can be used in NativeAOT applications without any issues.
+
+### What's AOT-Compatible
+
+- ✅ Core API functionality (`Api.Instance`, `GetClient()`, flag evaluations)
+- ✅ All built-in providers (`NoOpProvider`, etc.)
+- ✅ JSON serialization of `Value`, `Structure`, and `EvaluationContext`
+- ✅ Error handling and enum descriptions
+- ✅ Hook system
+- ✅ Event handling
+- ✅ Metrics collection
+- ✅ Basic dependency injection (with proper setup)
+
+### AOT Optimizations Made
+
+1. **Removed Reflection Usage**: Replaced reflection-based enum description reading with compile-time switch expressions
+2. **JSON Source Generation**: Added `OpenFeatureJsonSerializerContext` for AOT-compatible JSON serialization
+3. **Trimming Annotations**: Added appropriate trimming and AOT compatibility attributes
+4. **Manual JSON Converter**: The `ValueJsonConverter` uses manual JSON reading/writing instead of reflection
+
+## Using OpenFeature with NativeAOT
+
+### 1. Project Configuration
+
+To enable NativeAOT in your project, add these properties to your `.csproj` file:
+
+```xml
+
+
+ net8.0
+ Exe
+
+
+ true
+ true
+ true
+ full
+
+
+
+
+
+
+```
+
+### 2. Basic Usage
+
+```csharp
+using OpenFeature;
+using OpenFeature.Model;
+
+// Basic OpenFeature usage - fully AOT compatible
+var api = Api.Instance;
+var client = api.GetClient("my-app");
+
+// All flag evaluation methods work
+var boolFlag = await client.GetBooleanValueAsync("feature-enabled", false);
+var stringFlag = await client.GetStringValueAsync("welcome-message", "Hello");
+var intFlag = await client.GetIntegerValueAsync("max-items", 10);
+```
+
+### 3. JSON Serialization (Recommended)
+
+For optimal AOT performance, use the provided `JsonSerializerContext`:
+
+```csharp
+using System.Text.Json;
+using OpenFeature.Model;
+using OpenFeature.Serialization;
+
+var value = new Value(Structure.Builder()
+ .Set("name", "test")
+ .Set("enabled", true)
+ .Build());
+
+// Use AOT-compatible serialization
+var json = JsonSerializer.Serialize(value, OpenFeatureJsonSerializerContext.Default.Value);
+var deserialized = JsonSerializer.Deserialize(json, OpenFeatureJsonSerializerContext.Default.Value);
+```
+
+### 4. Error Handling
+
+Error types provide AOT-compatible descriptions:
+
+```csharp
+using OpenFeature.Constant;
+using OpenFeature.Extension;
+
+try
+{
+ var result = await client.GetBooleanValueAsync("missing-flag", false);
+}
+catch (FeatureProviderException ex)
+{
+ // GetDescription() is AOT-compatible
+ var errorDescription = ex.ErrorType.GetDescription();
+ Console.WriteLine($"Error: {errorDescription}");
+}
+```
+
+### 5. Publishing for NativeAOT
+
+Build and publish your AOT application:
+
+```bash
+# Build with AOT analysis
+dotnet build -c Release
+
+# Publish as native executable
+dotnet publish -c Release
+
+# Run the native executable (example path for macOS ARM64)
+./bin/Release/net9.0/osx-arm64/publish/MyApp
+```
+
+## Performance Benefits
+
+NativeAOT compilation provides several benefits:
+
+- **Faster Startup**: Native executables start faster than JIT-compiled applications
+- **Lower Memory Usage**: Reduced memory footprint
+- **Self-Contained**: No .NET runtime dependency required
+- **Smaller Deployment**: Optimized for size with trimming
+
+## Testing AOT Compatibility
+
+The SDK includes an AOT compatibility test project at `test/OpenFeature.AotCompatibility/` that:
+
+- Tests all core SDK functionality
+- Validates JSON serialization with source generation
+- Verifies error handling works correctly
+- Can be compiled and run as a native executable
+
+Run the test:
+
+```bash
+cd test/OpenFeature.AotCompatibility
+dotnet publish -c Release
+./bin/Release/net9.0/[runtime]/publish/OpenFeature.AotCompatibility
+```
+
+## Limitations
+
+Currently, there are no known limitations when using OpenFeature with NativeAOT. All core functionality is fully supported.
+
+## Provider Compatibility
+
+When using third-party providers, ensure they are also AOT-compatible. Check the provider's documentation for AOT support.
+
+## Troubleshooting
+
+### Trimming Warnings
+
+If you encounter trimming warnings, you can:
+
+1. Use the provided `JsonSerializerContext` for JSON operations
+2. Ensure your providers are AOT-compatible
+3. Add appropriate `[DynamicallyAccessedMembers]` attributes if needed
+
+### Build Issues
+
+- Ensure you're targeting .NET 8.0 or later
+- Verify all dependencies support NativeAOT
+- Check that `PublishAot` is set to `true`
+
+## Migration Guide
+
+If migrating from a non-AOT setup:
+
+1. **JSON Serialization**: Replace direct `JsonSerializer` calls with the provided context
+2. **Reflection**: The SDK no longer uses reflection, but ensure your custom code doesn't
+3. **Dynamic Loading**: Avoid dynamic assembly loading; register providers at compile time
+
+## Example AOT Application
+
+See the complete example in `test/OpenFeature.AotCompatibility/Program.cs` for a working AOT application that demonstrates all SDK features.
From 108691482194cd462d916837b518aedd402d5fa6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Mon, 18 Aug 2025 17:07:09 +0100
Subject: [PATCH 18/27] fix: streamline AOT compatibility documentation by
removing redundant sections and enhancing clarity
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
docs/AOT_COMPATIBILITY.md | 47 +++++++++------------------------------
1 file changed, 10 insertions(+), 37 deletions(-)
diff --git a/docs/AOT_COMPATIBILITY.md b/docs/AOT_COMPATIBILITY.md
index d242ae9b..7c663319 100644
--- a/docs/AOT_COMPATIBILITY.md
+++ b/docs/AOT_COMPATIBILITY.md
@@ -4,25 +4,18 @@ The OpenFeature .NET SDK is compatible with .NET NativeAOT compilation, allowing
## Compatibility Status
-✅ **Fully Compatible** - The SDK can be used in NativeAOT applications without any issues.
+**Fully Compatible** - The SDK can be used in NativeAOT applications without any issues.
### What's AOT-Compatible
-- ✅ Core API functionality (`Api.Instance`, `GetClient()`, flag evaluations)
-- ✅ All built-in providers (`NoOpProvider`, etc.)
-- ✅ JSON serialization of `Value`, `Structure`, and `EvaluationContext`
-- ✅ Error handling and enum descriptions
-- ✅ Hook system
-- ✅ Event handling
-- ✅ Metrics collection
-- ✅ Basic dependency injection (with proper setup)
-
-### AOT Optimizations Made
-
-1. **Removed Reflection Usage**: Replaced reflection-based enum description reading with compile-time switch expressions
-2. **JSON Source Generation**: Added `OpenFeatureJsonSerializerContext` for AOT-compatible JSON serialization
-3. **Trimming Annotations**: Added appropriate trimming and AOT compatibility attributes
-4. **Manual JSON Converter**: The `ValueJsonConverter` uses manual JSON reading/writing instead of reflection
+- Core API functionality (`Api.Instance`, `GetClient()`, flag evaluations)
+- All built-in providers (`NoOpProvider`, etc.)
+- JSON serialization of `Value`, `Structure`, and `EvaluationContext`
+- Error handling and enum descriptions
+- Hook system
+- Event handling
+- Metrics collection
+- Dependency injection
## Using OpenFeature with NativeAOT
@@ -84,27 +77,7 @@ var json = JsonSerializer.Serialize(value, OpenFeatureJsonSerializerContext.Defa
var deserialized = JsonSerializer.Deserialize(json, OpenFeatureJsonSerializerContext.Default.Value);
```
-### 4. Error Handling
-
-Error types provide AOT-compatible descriptions:
-
-```csharp
-using OpenFeature.Constant;
-using OpenFeature.Extension;
-
-try
-{
- var result = await client.GetBooleanValueAsync("missing-flag", false);
-}
-catch (FeatureProviderException ex)
-{
- // GetDescription() is AOT-compatible
- var errorDescription = ex.ErrorType.GetDescription();
- Console.WriteLine($"Error: {errorDescription}");
-}
-```
-
-### 5. Publishing for NativeAOT
+### 4. Publishing for NativeAOT
Build and publish your AOT application:
From f005f9e3271ed62e54f4bcc4fae5276275065b34 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Mon, 18 Aug 2025 17:19:23 +0100
Subject: [PATCH 19/27] fix: update actions/checkout and actions/cache versions
in AOT compatibility workflow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index f8ff3fdc..c3df528b 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -46,7 +46,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
submodules: recursive
@@ -57,7 +57,7 @@ jobs:
global-json-file: global.json
- name: Cache NuGet packages
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj') }}
From f36d20f231b8b7e42e5bf823858d762b272953a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Tue, 19 Aug 2025 16:21:26 +0100
Subject: [PATCH 20/27] Apply suggestions from code review
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Weihan Li
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
OpenFeature.slnx | 2 +-
build/Common.prod.props | 2 +-
.../OpenFeature.AotCompatibility.csproj | 4 ----
3 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/OpenFeature.slnx b/OpenFeature.slnx
index 03efdf49..c566e011 100644
--- a/OpenFeature.slnx
+++ b/OpenFeature.slnx
@@ -65,7 +65,7 @@
-
+
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 199961c6..7feb1759 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -25,7 +25,7 @@
-
+
true
diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
index 8bdea03d..b2f9710f 100644
--- a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
+++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
@@ -8,10 +8,7 @@
true
- true
true
- full
- Size
false
@@ -28,7 +25,6 @@
-
From d93dd2d228fabb70c2e2606d2f113746782edd19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Tue, 19 Aug 2025 16:21:44 +0100
Subject: [PATCH 21/27] Update .github/workflows/aot-compatibility.yml
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Weihan Li
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/aot-compatibility.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
index c3df528b..7d158474 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/aot-compatibility.yml
@@ -81,7 +81,6 @@ jobs:
shell: pwsh
run: |
dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj `
- -c Release `
-r ${{ matrix.runtime }} `
-o ./aot-output
From d05fa4e8328f71640743b8aee60f1632e044d14b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Tue, 19 Aug 2025 16:24:59 +0100
Subject: [PATCH 22/27] fix: remove unnecessary properties from AOT project
configuration
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
docs/AOT_COMPATIBILITY.md | 3 ---
1 file changed, 3 deletions(-)
diff --git a/docs/AOT_COMPATIBILITY.md b/docs/AOT_COMPATIBILITY.md
index 7c663319..afa6f1e7 100644
--- a/docs/AOT_COMPATIBILITY.md
+++ b/docs/AOT_COMPATIBILITY.md
@@ -31,9 +31,6 @@ To enable NativeAOT in your project, add these properties to your `.csproj` file
true
- true
- true
- full
From 5994f99b313db1a520c15c03e1595a582027027a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Tue, 19 Aug 2025 20:39:38 +0100
Subject: [PATCH 23/27] docs: update README to clarify NativeAOT compatibility
for contrib and community providers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
README.md | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 44af242c..c263023f 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,8 @@ Note that the packages will aim to support all current .NET versions. Refer to t
✅ **Full NativeAOT Compatibility** - The OpenFeature .NET SDK is fully compatible with .NET NativeAOT compilation for fast startup and small deployment size. See the [AOT Compatibility Guide](docs/AOT_COMPATIBILITY.md) for detailed instructions.
+> While the core OpenFeature SDK is fully NativeAOT compatible, contrib and community-provided providers, hooks, and extensions may not be. Please check with individual provider/hook documentation for their NativeAOT compatibility status.
+
### Install
Use the following to initialize your project:
@@ -724,12 +726,12 @@ For this hook to function correctly a global `MeterProvider` must be set.
Below are the metrics extracted by this hook and dimensions they carry:
-| Metric key | Description | Unit | Dimensions |
-| -------------------------------------- | ------------------------------- | ------------ | ----------------------------- |
-| feature_flag.evaluation_requests_total | Number of evaluation requests | request | key, provider name |
-| feature_flag.evaluation_success_total | Flag evaluation successes | impression | key, provider name, reason |
-| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
-| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |
+| Metric key | Description | Unit | Dimensions |
+| -------------------------------------- | ------------------------------- | ---------- | ----------------------------- |
+| feature_flag.evaluation_requests_total | Number of evaluation requests | request | key, provider name |
+| feature_flag.evaluation_success_total | Flag evaluation successes | impression | key, provider name, reason |
+| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
+| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |
Consider the following code example for usage.
From 14a881e500f384752db3417d508c90c8a7673c70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Wed, 20 Aug 2025 09:18:44 +0100
Subject: [PATCH 24/27] Apply suggestions from code review
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Weihan Li
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
Directory.Packages.props | 1 -
OpenFeature.slnx | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5f27a569..ea8fdef3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -36,7 +36,6 @@
-
diff --git a/OpenFeature.slnx b/OpenFeature.slnx
index c566e011..fa407cd3 100644
--- a/OpenFeature.slnx
+++ b/OpenFeature.slnx
@@ -64,7 +64,7 @@
-
+
From 92339a642e04de6e78b0350523de18d06d582cb2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 22 Aug 2025 11:08:37 +0100
Subject: [PATCH 25/27] fix: add descriptions to ErrorType enum values for
better clarity
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
src/OpenFeature/Constant/ErrorType.cs | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs
index 3717bcad..d36f3d96 100644
--- a/src/OpenFeature/Constant/ErrorType.cs
+++ b/src/OpenFeature/Constant/ErrorType.cs
@@ -1,3 +1,5 @@
+using System.ComponentModel;
+
namespace OpenFeature.Constant;
///
@@ -14,40 +16,40 @@ public enum ErrorType
///
/// Provider has yet been initialized
///
- ProviderNotReady,
+ [Description("PROVIDER_NOT_READY")] ProviderNotReady,
///
/// Provider was unable to find the flag
///
- FlagNotFound,
+ [Description("FLAG_NOT_FOUND")] FlagNotFound,
///
/// Provider failed to parse the flag response
///
- ParseError,
+ [Description("PARSE_ERROR")] ParseError,
///
/// Request type does not match the expected type
///
- TypeMismatch,
+ [Description("TYPE_MISMATCH")] TypeMismatch,
///
/// Abnormal execution of the provider
///
- General,
+ [Description("GENERAL")] General,
///
/// Context does not satisfy provider requirements.
///
- InvalidContext,
+ [Description("INVALID_CONTEXT")] InvalidContext,
///
/// Context does not contain a targeting key and the provider requires one.
///
- TargetingKeyMissing,
+ [Description("TARGETING_KEY_MISSING")] TargetingKeyMissing,
///
/// The provider has entered an irrecoverable error state.
///
- ProviderFatal,
+ [Description("PROVIDER_FATAL")] ProviderFatal,
}
From 67559c97ed51cecc1848b29c50c59d70ba9e41f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 22 Aug 2025 14:22:25 +0100
Subject: [PATCH 26/27] fix: remove AOT compatibility references and enhance
error handling tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
...OpenFeature.Providers.MultiProvider.csproj | 1 -
src/OpenFeature/OpenFeature.csproj | 1 -
test/OpenFeature.AotCompatibility/Program.cs | 42 ++++++++++++++++---
3 files changed, 37 insertions(+), 7 deletions(-)
diff --git a/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj b/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj
index c2e57daf..000f223b 100644
--- a/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj
+++ b/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj
@@ -9,7 +9,6 @@
-
diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj
index 0344bdbc..4a964ef5 100644
--- a/src/OpenFeature/OpenFeature.csproj
+++ b/src/OpenFeature/OpenFeature.csproj
@@ -21,7 +21,6 @@
-
diff --git a/test/OpenFeature.AotCompatibility/Program.cs b/test/OpenFeature.AotCompatibility/Program.cs
index 74692a31..5529eef2 100644
--- a/test/OpenFeature.AotCompatibility/Program.cs
+++ b/test/OpenFeature.AotCompatibility/Program.cs
@@ -3,7 +3,6 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenFeature.Constant;
-using OpenFeature.Extension;
using OpenFeature.Model;
using OpenFeature.Providers.MultiProvider;
using OpenFeature.Providers.MultiProvider.Models;
@@ -82,6 +81,23 @@ private static async Task TestBasicApiAsync()
.Build();
api.SetContext(context);
Console.WriteLine($"✓- Evaluation context set with {context.Count} attributes");
+
+ // Test error flag with AOT-compatible GetDescription()
+ await TestErrorFlagAsync(client);
+ }
+
+ private static async Task TestErrorFlagAsync(IFeatureClient client)
+ {
+ Console.WriteLine("\nTesting error flag with GetDescription()...");
+
+ // Set a test provider that can return errors
+ await Api.Instance.SetProviderAsync(new TestProvider());
+
+ // Test the error flag - this will internally trigger GetDescription() in the SDK's error handling
+ var errorResult = await client.GetBooleanDetailsAsync("error-flag", false);
+ Console.WriteLine($"✓- Error flag evaluation: {errorResult.Value} (Error: {errorResult.ErrorType})");
+ Console.WriteLine($"✓- Error message: '{errorResult.ErrorMessage}'");
+ Console.WriteLine("✓- GetDescription() method was executed internally by the SDK during error handling");
}
private static async Task TestMultiProviderAotCompatibilityAsync()
@@ -214,7 +230,7 @@ private static void TestErrorHandling()
{
Console.WriteLine("\nTesting error handling and enum descriptions...");
- // Test ErrorType descriptions (this was the main reflection usage we fixed)
+ // Test ErrorType enum values (GetDescription will be called internally by the SDK)
var errorTypes = new[]
{
ErrorType.None, ErrorType.ProviderNotReady, ErrorType.FlagNotFound, ErrorType.ParseError,
@@ -224,9 +240,12 @@ private static void TestErrorHandling()
foreach (var errorType in errorTypes)
{
- var description = errorType.GetDescription();
- Console.WriteLine($"✓- {errorType}: '{description}'");
+ // Just validate the enum values exist and are accessible in AOT
+ Console.WriteLine($"✓- ErrorType.{errorType} is accessible in AOT compilation");
}
+
+ Console.WriteLine("✓- All ErrorType enum values validated for AOT compatibility");
+ Console.WriteLine("✓- GetDescription() method will be exercised internally when errors occur");
}
}
@@ -239,7 +258,20 @@ internal class TestProvider : FeatureProvider
public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
EvaluationContext? context = null, CancellationToken cancellationToken = default)
- => Task.FromResult(new ResolutionDetails(flagKey, true));
+ {
+ if (flagKey == "error-flag")
+ {
+ // Return an error for the "error-flag" key using constructor parameters
+ return Task.FromResult(new ResolutionDetails(
+ flagKey: flagKey,
+ value: defaultValue,
+ errorType: ErrorType.FlagNotFound,
+ errorMessage: "The flag key was not found."
+ ));
+ }
+
+ return Task.FromResult(new ResolutionDetails(flagKey, true));
+ }
public override Task> ResolveStringValueAsync(string flagKey, string defaultValue,
EvaluationContext? context = null, CancellationToken cancellationToken = default)
From 2cadd2a62c21ffc6b5c9871271029dd09a9c0ba6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 22 Aug 2025 14:25:36 +0100
Subject: [PATCH 27/27] fix: update System.Text.Json package reference in
project files
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
Directory.Packages.props | 3 +--
.../OpenFeature.AotCompatibility.csproj | 4 ++++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ea8fdef3..8f655078 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,8 +23,7 @@
-
+
diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
index b2f9710f..d416bd75 100644
--- a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
+++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj
@@ -27,4 +27,8 @@
+
+
+
+