Skip to content

Commit acd0486

Browse files
askptWeihanLi
andauthored
perf: Add NativeAOT Support (#554)
* feat: enhance ValueJsonConverter for AOT compatibility with manual JSON handling Signed-off-by: André Silva <[email protected]> * feat: refactor EnumExtensions to improve AOT compatibility and remove reflection Signed-off-by: André Silva <[email protected]> * feat: add OpenFeatureJsonSerializerContext for AOT compilation support Signed-off-by: André Silva <[email protected]> * feat: add AOT and trimming support for net8.0 and net9.0 Signed-off-by: André Silva <[email protected]> * feat: add NativeAOT compatibility tests and project configuration Signed-off-by: André Silva <[email protected]> * feat: update project structure for AOT compatibility and add MultiProvider tests Signed-off-by: André Silva <[email protected]> * fix: remove unnecessary Type attribute project in solution file Signed-off-by: André Silva <[email protected]> * feat: add unit tests for EnumExtensions.GetDescription method Signed-off-by: André Silva <[email protected]> * fix: remove trimming support properties for net8.0 and net9.0 Signed-off-by: André Silva <[email protected]> * feat: add AOT compatibility workflow with cross-platform testing and size comparison Signed-off-by: André Silva <[email protected]> * fix: simplify AOT compatibility workflow by removing unnecessary properties and AspNetCore sample tests Signed-off-by: André Silva <[email protected]> * fix: update AOT compatibility workflow to include runtime in publish step and standardize comment formatting Signed-off-by: André Silva <[email protected]> * fix: update AOT compatibility workflow to streamline ARM64 handling and switch to PowerShell for script execution Signed-off-by: André Silva <[email protected]> * fix: standardize shell usage and update publish command syntax in AOT compatibility workflow Signed-off-by: André Silva <[email protected]> * fix: update AOT size comparison report to remove AspNetCore sample column and enhance binary size logging Signed-off-by: André Silva <[email protected]> * fix: remove AOT size comparison job and artifact upload steps from workflow Signed-off-by: André Silva <[email protected]> * fix: update AOT compatibility workflow permissions and enhance documentation for NativeAOT support Signed-off-by: André Silva <[email protected]> * fix: streamline AOT compatibility documentation by removing redundant sections and enhancing clarity Signed-off-by: André Silva <[email protected]> * fix: update actions/checkout and actions/cache versions in AOT compatibility workflow Signed-off-by: André Silva <[email protected]> * Apply suggestions from code review Co-authored-by: Weihan Li <[email protected]> Signed-off-by: André Silva <[email protected]> * Update .github/workflows/aot-compatibility.yml Co-authored-by: Weihan Li <[email protected]> Signed-off-by: André Silva <[email protected]> * fix: remove unnecessary properties from AOT project configuration Signed-off-by: André Silva <[email protected]> * docs: update README to clarify NativeAOT compatibility for contrib and community providers Signed-off-by: André Silva <[email protected]> * Apply suggestions from code review Co-authored-by: Weihan Li <[email protected]> Signed-off-by: André Silva <[email protected]> * fix: add descriptions to ErrorType enum values for better clarity Signed-off-by: André Silva <[email protected]> * fix: remove AOT compatibility references and enhance error handling tests Signed-off-by: André Silva <[email protected]> * fix: update System.Text.Json package reference in project files Signed-off-by: André Silva <[email protected]> --------- Signed-off-by: André Silva <[email protected]> Co-authored-by: Weihan Li <[email protected]>
1 parent 4915f2b commit acd0486

File tree

13 files changed

+686
-19
lines changed

13 files changed

+686
-19
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: AOT Compatibility
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
merge_group:
9+
workflow_dispatch:
10+
11+
jobs:
12+
aot-compatibility:
13+
name: AOT Test (${{ matrix.os }}, ${{ matrix.arch }})
14+
permissions:
15+
contents: read
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
include:
20+
# Linux x64
21+
- os: ubuntu-latest
22+
arch: x64
23+
runtime: linux-x64
24+
# Linux ARM64
25+
- os: ubuntu-24.04-arm
26+
arch: arm64
27+
runtime: linux-arm64
28+
# Windows x64
29+
- os: windows-latest
30+
arch: x64
31+
runtime: win-x64
32+
# Windows ARM64
33+
- os: windows-11-arm
34+
arch: arm64
35+
runtime: win-arm64
36+
# macOS x64
37+
- os: macos-13
38+
arch: x64
39+
runtime: osx-x64
40+
# macOS ARM64 (Apple Silicon)
41+
- os: macos-latest
42+
arch: arm64
43+
runtime: osx-arm64
44+
45+
runs-on: ${{ matrix.os }}
46+
47+
steps:
48+
- name: Checkout
49+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
50+
with:
51+
fetch-depth: 0
52+
submodules: recursive
53+
54+
- name: Setup .NET SDK
55+
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
56+
with:
57+
global-json-file: global.json
58+
59+
- name: Cache NuGet packages
60+
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
61+
with:
62+
path: ~/.nuget/packages
63+
key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj') }}
64+
restore-keys: |
65+
${{ runner.os }}-${{ matrix.arch }}-nuget-
66+
${{ runner.os }}-nuget-
67+
68+
- name: Restore dependencies
69+
shell: pwsh
70+
run: dotnet restore
71+
72+
- name: Build solution
73+
shell: pwsh
74+
run: dotnet build -c Release --no-restore
75+
76+
- name: Test AOT compatibility project build
77+
shell: pwsh
78+
run: dotnet build test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj -c Release --no-restore
79+
80+
- name: Publish AOT compatibility test (cross-platform)
81+
shell: pwsh
82+
run: |
83+
dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj `
84+
-r ${{ matrix.runtime }} `
85+
-o ./aot-output
86+
87+
- name: Run AOT compatibility test
88+
shell: pwsh
89+
run: |
90+
if ("${{ runner.os }}" -eq "Windows") {
91+
./aot-output/OpenFeature.AotCompatibility.exe
92+
} else {
93+
chmod +x ./aot-output/OpenFeature.AotCompatibility
94+
./aot-output/OpenFeature.AotCompatibility
95+
}

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
<PackageVersion Include="System.Collections.Immutable" Version="$(MicrosoftExtensionsVersion)" />
2424
<PackageVersion Include="System.Diagnostics.DiagnosticSource"
2525
Version="$(MicrosoftExtensionsVersion)" />
26-
<PackageVersion Include="System.Text.Json"
27-
Version="8.0.5" />
26+
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
2827
<PackageVersion Include="System.Threading.Channels" Version="$(MicrosoftExtensionsVersion)" />
2928
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
3029
</ItemGroup>
@@ -36,6 +35,7 @@
3635
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
3736
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
3837
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="9.3.0" />
38+
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsVersion)" />
3939
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
4040
<PackageVersion Include="NSubstitute" Version="5.3.0" />
4141
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />

OpenFeature.slnx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
<Folder Name="/src/">
5454
<Project Path="src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj" />
5555
<Project Path="src/OpenFeature.Hosting/OpenFeature.Hosting.csproj" />
56-
<Project Path="src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj" Type="Classic C#" />
56+
<Project Path="src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj" />
5757
<Project Path="src/OpenFeature/OpenFeature.csproj" />
5858
<File Path="src/Directory.Build.props" />
5959
<File Path="src/Directory.Build.targets" />
@@ -64,7 +64,8 @@
6464
<Project Path="test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj" />
6565
<Project Path="test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj" />
6666
<Project Path="test/OpenFeature.Tests/OpenFeature.Tests.csproj" />
67-
<Project Path="test\OpenFeature.Providers.MultiProvider.Tests\OpenFeature.Providers.MultiProvider.Tests.csproj" Type="Classic C#" />
67+
<Project Path="test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj" />
68+
<Project Path="test/OpenFeature.Providers.MultiProvider.Tests/OpenFeature.Providers.MultiProvider.Tests.csproj" />
6869
<File Path="test/Directory.Build.props" />
6970
</Folder>
70-
</Solution>
71+
</Solution>

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333

3434
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
3535

36+
### NativeAOT Support
37+
38+
**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.
39+
40+
> 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.
41+
3642
### Install
3743

3844
Use the following to initialize your project:
@@ -720,12 +726,12 @@ For this hook to function correctly a global `MeterProvider` must be set.
720726
721727
Below are the metrics extracted by this hook and dimensions they carry:
722728
723-
| Metric key | Description | Unit | Dimensions |
724-
| -------------------------------------- | ------------------------------- | ------------ | ----------------------------- |
725-
| feature_flag.evaluation_requests_total | Number of evaluation requests | request | key, provider name |
726-
| feature_flag.evaluation_success_total | Flag evaluation successes | impression | key, provider name, reason |
727-
| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
728-
| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |
729+
| Metric key | Description | Unit | Dimensions |
730+
| -------------------------------------- | ------------------------------- | ---------- | ----------------------------- |
731+
| feature_flag.evaluation_requests_total | Number of evaluation requests | request | key, provider name |
732+
| feature_flag.evaluation_success_total | Flag evaluation successes | impression | key, provider name, reason |
733+
| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
734+
| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |
729735
730736
Consider the following code example for usage.
731737

build/Common.prod.props

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
2-
<Import Project=".\Common.props" />
2+
<Import Project=".\Common.props"/>
33

44
<PropertyGroup>
55
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
@@ -24,8 +24,13 @@
2424
<FileVersion>$(VersionNumber)</FileVersion>
2525
</PropertyGroup>
2626

27+
<!-- AOT and Trimming Support -->
28+
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
29+
<IsAotCompatible>true</IsAotCompatible>
30+
</PropertyGroup>
31+
2732
<ItemGroup>
28-
<None Include="$(MSBuildThisFileDirectory)openfeature-icon.png" Pack="true" PackagePath="\" />
33+
<None Include="$(MSBuildThisFileDirectory)openfeature-icon.png" Pack="true" PackagePath="\"/>
2934
</ItemGroup>
3035

3136
</Project>

docs/AOT_COMPATIBILITY.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# OpenFeature .NET SDK - NativeAOT Compatibility
2+
3+
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.
4+
5+
## Compatibility Status
6+
7+
**Fully Compatible** - The SDK can be used in NativeAOT applications without any issues.
8+
9+
### What's AOT-Compatible
10+
11+
- Core API functionality (`Api.Instance`, `GetClient()`, flag evaluations)
12+
- All built-in providers (`NoOpProvider`, etc.)
13+
- JSON serialization of `Value`, `Structure`, and `EvaluationContext`
14+
- Error handling and enum descriptions
15+
- Hook system
16+
- Event handling
17+
- Metrics collection
18+
- Dependency injection
19+
20+
## Using OpenFeature with NativeAOT
21+
22+
### 1. Project Configuration
23+
24+
To enable NativeAOT in your project, add these properties to your `.csproj` file:
25+
26+
```xml
27+
<Project Sdk="Microsoft.NET.Sdk">
28+
<PropertyGroup>
29+
<TargetFramework>net8.0</TargetFramework> <!-- or net9.0 -->
30+
<OutputType>Exe</OutputType>
31+
32+
<!-- Enable NativeAOT -->
33+
<PublishAot>true</PublishAot>
34+
</PropertyGroup>
35+
36+
<ItemGroup>
37+
<PackageReference Include="OpenFeature" Version="2.x.x" />
38+
</ItemGroup>
39+
</Project>
40+
```
41+
42+
### 2. Basic Usage
43+
44+
```csharp
45+
using OpenFeature;
46+
using OpenFeature.Model;
47+
48+
// Basic OpenFeature usage - fully AOT compatible
49+
var api = Api.Instance;
50+
var client = api.GetClient("my-app");
51+
52+
// All flag evaluation methods work
53+
var boolFlag = await client.GetBooleanValueAsync("feature-enabled", false);
54+
var stringFlag = await client.GetStringValueAsync("welcome-message", "Hello");
55+
var intFlag = await client.GetIntegerValueAsync("max-items", 10);
56+
```
57+
58+
### 3. JSON Serialization (Recommended)
59+
60+
For optimal AOT performance, use the provided `JsonSerializerContext`:
61+
62+
```csharp
63+
using System.Text.Json;
64+
using OpenFeature.Model;
65+
using OpenFeature.Serialization;
66+
67+
var value = new Value(Structure.Builder()
68+
.Set("name", "test")
69+
.Set("enabled", true)
70+
.Build());
71+
72+
// Use AOT-compatible serialization
73+
var json = JsonSerializer.Serialize(value, OpenFeatureJsonSerializerContext.Default.Value);
74+
var deserialized = JsonSerializer.Deserialize(json, OpenFeatureJsonSerializerContext.Default.Value);
75+
```
76+
77+
### 4. Publishing for NativeAOT
78+
79+
Build and publish your AOT application:
80+
81+
```bash
82+
# Build with AOT analysis
83+
dotnet build -c Release
84+
85+
# Publish as native executable
86+
dotnet publish -c Release
87+
88+
# Run the native executable (example path for macOS ARM64)
89+
./bin/Release/net9.0/osx-arm64/publish/MyApp
90+
```
91+
92+
## Performance Benefits
93+
94+
NativeAOT compilation provides several benefits:
95+
96+
- **Faster Startup**: Native executables start faster than JIT-compiled applications
97+
- **Lower Memory Usage**: Reduced memory footprint
98+
- **Self-Contained**: No .NET runtime dependency required
99+
- **Smaller Deployment**: Optimized for size with trimming
100+
101+
## Testing AOT Compatibility
102+
103+
The SDK includes an AOT compatibility test project at `test/OpenFeature.AotCompatibility/` that:
104+
105+
- Tests all core SDK functionality
106+
- Validates JSON serialization with source generation
107+
- Verifies error handling works correctly
108+
- Can be compiled and run as a native executable
109+
110+
Run the test:
111+
112+
```bash
113+
cd test/OpenFeature.AotCompatibility
114+
dotnet publish -c Release
115+
./bin/Release/net9.0/[runtime]/publish/OpenFeature.AotCompatibility
116+
```
117+
118+
## Limitations
119+
120+
Currently, there are no known limitations when using OpenFeature with NativeAOT. All core functionality is fully supported.
121+
122+
## Provider Compatibility
123+
124+
When using third-party providers, ensure they are also AOT-compatible. Check the provider's documentation for AOT support.
125+
126+
## Troubleshooting
127+
128+
### Trimming Warnings
129+
130+
If you encounter trimming warnings, you can:
131+
132+
1. Use the provided `JsonSerializerContext` for JSON operations
133+
2. Ensure your providers are AOT-compatible
134+
3. Add appropriate `[DynamicallyAccessedMembers]` attributes if needed
135+
136+
### Build Issues
137+
138+
- Ensure you're targeting .NET 8.0 or later
139+
- Verify all dependencies support NativeAOT
140+
- Check that `PublishAot` is set to `true`
141+
142+
## Migration Guide
143+
144+
If migrating from a non-AOT setup:
145+
146+
1. **JSON Serialization**: Replace direct `JsonSerializer` calls with the provided context
147+
2. **Reflection**: The SDK no longer uses reflection, but ensure your custom code doesn't
148+
3. **Dynamic Loading**: Avoid dynamic assembly loading; register providers at compile time
149+
150+
## Example AOT Application
151+
152+
See the complete example in `test/OpenFeature.AotCompatibility/Program.cs` for a working AOT application that demonstrates all SDK features.
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
1-
using System.ComponentModel;
1+
using OpenFeature.Constant;
22

33
namespace OpenFeature.Extension;
44

55
internal static class EnumExtensions
66
{
7+
/// <summary>
8+
/// Gets the description of an enum value without using reflection.
9+
/// This is AOT-compatible and only supports specific known enum types.
10+
/// </summary>
11+
/// <param name="value">The enum value to get the description for</param>
12+
/// <returns>The description string or the enum value as string if no description is available</returns>
713
public static string GetDescription(this Enum value)
814
{
9-
var field = value.GetType().GetField(value.ToString());
10-
var attribute = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
11-
return attribute?.Description ?? value.ToString();
15+
return value switch
16+
{
17+
// ErrorType descriptions
18+
ErrorType.None => "NONE",
19+
ErrorType.ProviderNotReady => "PROVIDER_NOT_READY",
20+
ErrorType.FlagNotFound => "FLAG_NOT_FOUND",
21+
ErrorType.ParseError => "PARSE_ERROR",
22+
ErrorType.TypeMismatch => "TYPE_MISMATCH",
23+
ErrorType.General => "GENERAL",
24+
ErrorType.InvalidContext => "INVALID_CONTEXT",
25+
ErrorType.TargetingKeyMissing => "TARGETING_KEY_MISSING",
26+
ErrorType.ProviderFatal => "PROVIDER_FATAL",
27+
28+
// Fallback for any other enum types
29+
_ => value.ToString()
30+
};
1231
}
1332
}

src/OpenFeature/Model/ValueJsonConverter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace OpenFeature.Model;
66

77
/// <summary>
8-
/// A <see cref="JsonConverter{T}"/> for <see cref="Value"/> for Json serialization
8+
/// A <see cref="JsonConverter{T}"/> for <see cref="Value"/> for Json serialization.
9+
/// This converter is AOT-compatible as it uses manual JSON reading/writing
10+
/// instead of reflection-based serialization.
911
/// </summary>
1012
public sealed class ValueJsonConverter : JsonConverter<Value>
1113
{

src/OpenFeature/OpenFeature.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
<None Include="../../README.md" Pack="true" PackagePath="/" />
2525
</ItemGroup>
2626

27-
</Project>
27+
</Project>

0 commit comments

Comments
 (0)