Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions samples/Console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ The sample defines the following flags using the `InMemoryProvider`:
## NativeAOT

This sample is published with [NativeAOT](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) enabled (`PublishAot=true`), demonstrating that the OpenFeature .NET SDK is fully compatible with NativeAOT compilation. See the [AOT Compatibility Guide](../../../docs/AOT_COMPATIBILITY.md) for more details.

## Isolated API Instance

The sample also demonstrates how to create an **isolated API instance** using `Api.CreateIsolated()`. The isolated instance has its own provider with different flag values, proving that it operates independently from the global singleton.

| Flag Key | Type | Variants | Default Variant |
| -------------- | -------- | ---------------------------------------------------------------------------------- | --------------- |
| `bool-flag` | `bool` | `on` → `true`, `off` → `false` | `off` |
| `string-flag` | `string` | `greeting` → `"Howdy, Isolated World!"`, `farewell` → `"See ya!"` | `greeting` |

The isolated instance evaluates flags from its own provider, then shuts down without affecting the global singleton. This is useful for testing, multi-tenant applications, and dependency injection scenarios. See the [specification](https://openfeature.dev/specification/sections/flag-evaluation#18-isolated-api-instances) for more details.
29 changes: 29 additions & 0 deletions samples/Console/app.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,32 @@
// Evaluate the `object-flag` flag and print the result to the console
var objectResult = await client.GetObjectValueAsync("object-flag", new Value("Ben"));
Console.WriteLine("The `object-flag` flag returned {0}", objectResult.AsString);

// --- Isolated API Instance ---
// Create an independent API instance with its own provider and state.
// This is useful for testing, multi-tenant apps, and DI scenarios.

var isolatedFlags = new Dictionary<string, Flag>
{
{ "bool-flag", new Flag<bool>(new Dictionary<string, bool> { { "on", true }, { "off", false } }, defaultVariant: "off") },
{ "string-flag", new Flag<string>(new Dictionary<string, string> { { "greeting", "Howdy, Isolated World!" }, { "farewell", "See ya!" } }, defaultVariant: "greeting") },
};

var isolated = Api.CreateIsolated();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the earlier comments; the namespace move and [Experimental] attribute look great! If I'm not mistaken though, this spot seems to have been missed; Api.CreateIsolated() no longer exists, so I think this should be OpenFeatureFactory.CreateIsolated() (with a using OpenFeature.Isolated; added at the top).

Copy link
Copy Markdown
Member Author

@askpt askpt May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...

Good catch...

Copy link
Copy Markdown
Member Author

@askpt askpt May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await isolated.SetProviderAsync(new InMemoryProvider(isolatedFlags));

var isolatedClient = isolated.GetClient();

Console.WriteLine();
Console.WriteLine("--- Isolated API Instance ---");

// The isolated instance has its own flag values
var isolatedBool = await isolatedClient.GetBooleanValueAsync("bool-flag", true);
Console.WriteLine("Isolated `bool-flag`: {0} (singleton was: {1})", isolatedBool, helloWorldResult);

var isolatedString = await isolatedClient.GetStringValueAsync("string-flag", "default");
Console.WriteLine("Isolated `string-flag`: {0} (singleton was: {1})", isolatedString, stringResult);

// Shut down the isolated instance — does not affect the global singleton
await isolated.ShutdownAsync();
Console.WriteLine("Isolated instance shut down. Singleton is unaffected.");
17 changes: 17 additions & 0 deletions src/OpenFeature/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public Task SetProviderAsync(FeatureProvider featureProvider)
/// <returns>A <see cref="Task"/> that completes once Provider initialization is complete.</returns>
public async Task SetProviderAsync(FeatureProvider featureProvider, CancellationToken cancellationToken)
{
this.ValidateProviderOwnership(featureProvider);
this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider);
await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitializationAsync, this.AfterErrorAsync, cancellationToken)
.ConfigureAwait(false);
Expand Down Expand Up @@ -91,6 +92,7 @@ public async Task SetProviderAsync(string domain, FeatureProvider featureProvide
{
throw new ArgumentNullException(nameof(domain));
}
this.ValidateProviderOwnership(featureProvider);
this._eventExecutor.RegisterClientFeatureProvider(domain, featureProvider);
await this._repository.SetProviderAsync(domain, featureProvider, this.GetContext(), this.AfterInitializationAsync, this.AfterErrorAsync, cancellationToken)
.ConfigureAwait(false);
Expand Down Expand Up @@ -401,4 +403,19 @@ internal static void SetInstance(Api api)
{
Instance = api;
}

/// <summary>
/// Validates that the given provider is not already bound to a different API instance.
/// </summary>
/// <param name="featureProvider">The provider to validate ownership for.</param>
/// <exception cref="InvalidOperationException">Thrown if the provider is already bound to a different API instance.</exception>
private void ValidateProviderOwnership(FeatureProvider featureProvider)
{
if (!featureProvider.TryBindApiInstance(this))
{
throw new InvalidOperationException(
"This provider instance is already bound to a different API instance. " +
"A provider should not be registered with more than one API instance simultaneously.");
}
}
}
38 changes: 38 additions & 0 deletions src/OpenFeature/Constant/FeatureDiagnosticCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace OpenFeature.Constant;

/// <summary>
/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework.
/// </summary>
/// <remarks>
/// <c>Experimental</c> - This class includes identifiers that allow developers to track and conditionally enable
/// experimental features. Each identifier follows a structured code format to indicate the feature domain,
/// maturity level, and unique identifier. Note that experimental features are subject to change or removal
/// in future releases.
/// <para>
/// <strong>Basic Information</strong><br/>
/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize
/// and manage experimental features effectively.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// Code Structure:
/// - "OF" - Represents the OpenFeature library.
/// - "ISO" - Indicates the Isolated API domain.
/// - "001" - Unique identifier for a specific feature.
/// </code>
/// </example>
internal static class FeatureDiagnosticCodes
{
/// <summary>
/// Identifier for the experimental Isolated API features within the OpenFeature framework.
/// </summary>
/// <remarks>
/// <c>OFISO001</c> identifier marks experimental features in the Isolated (ISO) domain.
///
/// Usage:
/// Developers can use this identifier to conditionally enable or test experimental Isolated API features.
/// It is part of the OpenFeature diagnostics system to help track experimental functionality.
/// </remarks>
public const string IsolatedApi = "OFISO001";
}
27 changes: 27 additions & 0 deletions src/OpenFeature/FeatureProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,33 @@ public abstract Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string
/// </summary>
internal virtual ProviderStatus Status { get; set; } = ProviderStatus.NotReady;

/// <summary>
/// Tracks which Api instance this provider is currently bound to.
/// A provider should not be registered with more than one API instance simultaneously (spec 1.8.4).
/// </summary>
private Api? _boundApiInstance;

/// <summary>
/// Attempts to bind this provider to the given API instance.
/// Uses <see cref="Interlocked.CompareExchange{T}"/> for thread-safe check-and-set.
/// </summary>
/// <param name="api">The API instance to bind to.</param>
/// <returns><c>true</c> if the provider was successfully bound (or was already bound to the same instance);
/// <c>false</c> if the provider is already bound to a different API instance.</returns>
internal bool TryBindApiInstance(Api api)
{
var previous = Interlocked.CompareExchange(ref this._boundApiInstance, api, null);
return previous is null || ReferenceEquals(previous, api);
}

/// <summary>
/// Clears the API instance binding, allowing this provider to be registered with another API instance.
/// </summary>
internal void UnbindApiInstance()
{
this._boundApiInstance = null;
}

/// <summary>
/// <para>
/// This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
Expand Down
36 changes: 36 additions & 0 deletions src/OpenFeature/Isolated/OpenFeatureFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if NET8_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif

namespace OpenFeature.Isolated;

/// <summary>
/// Factory for creating isolated instances of the OpenFeature API.
/// </summary>
/// <remarks>
/// This factory provides a method to create new, independent instances of the OpenFeature API with fully isolated state. Each isolated instance maintains its own providers, evaluation context, hooks, event handlers,
/// and transaction context propagators. It does not share state with the global <see cref="Api.Instance"/> singleton or with any other isolated instance.
/// </remarks>
public static class OpenFeatureFactory
{
/// <summary>
/// Creates a new, independent instance of the OpenFeature API with fully isolated state.
/// <para>
/// Each isolated instance maintains its own providers, evaluation context, hooks, event handlers,
/// and transaction context propagators. It does not share state with the global <see cref="Api.Instance"/>
/// singleton or with any other isolated instance.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// A single provider instance should not be bound to more than one API instance at a time.
/// Attempting to do so will result in an <see cref="InvalidOperationException"/>.
/// </para>
/// </remarks>
/// <returns>A new, independent <see cref="Api"/> instance.</returns>
/// <seealso href="https://openfeature.dev/specification/sections/flag-evaluation#18-isolated-api-instances">Specification 1.8 - Isolated API Instances</seealso>
#if NET8_0_OR_GREATER
[Experimental(Constant.FeatureDiagnosticCodes.IsolatedApi)]
#endif
public static Api CreateIsolated() => new Api();
}
13 changes: 13 additions & 0 deletions src/OpenFeature/ProviderRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ private async Task ShutdownIfUnusedAsync(
return;
}

// Clear ownership while still under the write lock — the provider is confirmed unused.
Comment thread
askpt marked this conversation as resolved.
// This prevents a race where async shutdown clears ownership after a re-registration.
if (targetProvider != null)
{
targetProvider.UnbindApiInstance();
}

await this.SafeShutdownProviderAsync(targetProvider, cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -270,6 +277,12 @@ internal async Task ShutdownAsync(Action<FeatureProvider, Exception>? afterError
// Set a default provider so the Api is ready to be used again.
this._defaultProvider = new NoOpFeatureProvider();
this._featureProviders.Clear();

// Clear ownership under the write lock for all providers being shut down.
foreach (var provider in providers)
{
provider.UnbindApiInstance();
}
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsNotAsErrors>NU1903</WarningsNotAsErrors>
<RootNamespace>OpenFeature.AotCompatibility</RootNamespace>
<NoWarn>$(NoWarn);OFISO001</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand All @@ -27,8 +28,4 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" />
</ItemGroup>

</Project>
39 changes: 39 additions & 0 deletions test/OpenFeature.AotCompatibility/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenFeature.Constant;
using OpenFeature.Isolated;
using OpenFeature.Model;
using OpenFeature.Providers.MultiProvider;
using OpenFeature.Providers.MultiProvider.Models;
Expand All @@ -27,6 +28,9 @@ private static async Task Main(string[] args)
// Test basic API functionality
await TestBasicApiAsync();

// Test isolated API instances
await TestIsolatedApiAsync();

// Test MultiProvider AOT compatibility
await TestMultiProviderAotCompatibilityAsync();

Expand All @@ -49,6 +53,41 @@ private static async Task Main(string[] args)
}
}

private static async Task TestIsolatedApiAsync()
{
Console.WriteLine("\nTesting isolated API instances...");

// Create an isolated instance
var isolated = OpenFeatureFactory.CreateIsolated();
Console.WriteLine($"✓- Isolated API instance created: {isolated.GetType().Name}");

// Verify it is distinct from the singleton
if (ReferenceEquals(isolated, Api.Instance))
{
throw new InvalidOperationException("Isolated instance should not be the same as the singleton.");
}
Console.WriteLine("✓- Isolated instance is distinct from singleton");

// Set a provider on the isolated instance
var provider = new TestProvider();
await isolated.SetProviderAsync(provider);
Console.WriteLine($"✓- Provider set on isolated instance: {isolated.GetProviderMetadata()?.Name}");

// Create a client and evaluate a flag
var client = isolated.GetClient("isolated-client");
var result = await client.GetBooleanValueAsync("test-flag", false);
Console.WriteLine($"✓- Isolated client flag evaluation: {result}");

// Set context on isolated instance and verify independence
var context = EvaluationContext.Builder().Set("scope", "isolated").Build();
isolated.SetContext(context);
Console.WriteLine($"✓- Isolated context set with {context.Count} attributes");

// Shutdown the isolated instance
await isolated.ShutdownAsync();
Console.WriteLine("✓- Isolated instance shut down successfully");
}

private static async Task TestBasicApiAsync()
{
Console.WriteLine("\nTesting basic API functionality...");
Expand Down
Loading
Loading