diff --git a/samples/Console/README.md b/samples/Console/README.md index 9470a776..fac135d7 100644 --- a/samples/Console/README.md +++ b/samples/Console/README.md @@ -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. diff --git a/samples/Console/app.cs b/samples/Console/app.cs index 9f5390d3..e780fac1 100644 --- a/samples/Console/app.cs +++ b/samples/Console/app.cs @@ -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 +{ + { "bool-flag", new Flag(new Dictionary { { "on", true }, { "off", false } }, defaultVariant: "off") }, + { "string-flag", new Flag(new Dictionary { { "greeting", "Howdy, Isolated World!" }, { "farewell", "See ya!" } }, defaultVariant: "greeting") }, +}; + +var isolated = Api.CreateIsolated(); +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."); diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 3532e99e..8e4b83dd 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -56,6 +56,7 @@ public Task SetProviderAsync(FeatureProvider featureProvider) /// A that completes once Provider initialization is complete. 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); @@ -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); @@ -401,4 +403,19 @@ internal static void SetInstance(Api api) { Instance = api; } + + /// + /// Validates that the given provider is not already bound to a different API instance. + /// + /// The provider to validate ownership for. + /// Thrown if the provider is already bound to a different API instance. + 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."); + } + } } diff --git a/src/OpenFeature/Constant/FeatureDiagnosticCodes.cs b/src/OpenFeature/Constant/FeatureDiagnosticCodes.cs new file mode 100644 index 00000000..dae01f60 --- /dev/null +++ b/src/OpenFeature/Constant/FeatureDiagnosticCodes.cs @@ -0,0 +1,38 @@ +namespace OpenFeature.Constant; + +/// +/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework. +/// +/// +/// Experimental - 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. +/// +/// Basic Information
+/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize +/// and manage experimental features effectively. +///
+///
+/// +/// +/// Code Structure: +/// - "OF" - Represents the OpenFeature library. +/// - "ISO" - Indicates the Isolated API domain. +/// - "001" - Unique identifier for a specific feature. +/// +/// +internal static class FeatureDiagnosticCodes +{ + /// + /// Identifier for the experimental Isolated API features within the OpenFeature framework. + /// + /// + /// OFISO001 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. + /// + public const string IsolatedApi = "OFISO001"; +} diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 16c495c6..ddefface 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -98,6 +98,33 @@ public abstract Task> ResolveStructureValueAsync(string /// internal virtual ProviderStatus Status { get; set; } = ProviderStatus.NotReady; + /// + /// 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). + /// + private Api? _boundApiInstance; + + /// + /// Attempts to bind this provider to the given API instance. + /// Uses for thread-safe check-and-set. + /// + /// The API instance to bind to. + /// true if the provider was successfully bound (or was already bound to the same instance); + /// false if the provider is already bound to a different API instance. + internal bool TryBindApiInstance(Api api) + { + var previous = Interlocked.CompareExchange(ref this._boundApiInstance, api, null); + return previous is null || ReferenceEquals(previous, api); + } + + /// + /// Clears the API instance binding, allowing this provider to be registered with another API instance. + /// + internal void UnbindApiInstance() + { + this._boundApiInstance = null; + } + /// /// /// This method is called before a provider is used to evaluate flags. Providers can overwrite this method, diff --git a/src/OpenFeature/Isolated/OpenFeatureFactory.cs b/src/OpenFeature/Isolated/OpenFeatureFactory.cs new file mode 100644 index 00000000..bcc4e13a --- /dev/null +++ b/src/OpenFeature/Isolated/OpenFeatureFactory.cs @@ -0,0 +1,36 @@ +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace OpenFeature.Isolated; + +/// +/// Factory for creating isolated instances of the OpenFeature API. +/// +/// +/// 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 singleton or with any other isolated instance. +/// +public static class OpenFeatureFactory +{ + /// + /// Creates a new, independent instance 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 + /// singleton or with any other isolated instance. + /// + /// + /// + /// + /// 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 . + /// + /// + /// A new, independent instance. + /// Specification 1.8 - Isolated API Instances +#if NET8_0_OR_GREATER + [Experimental(Constant.FeatureDiagnosticCodes.IsolatedApi)] +#endif + public static Api CreateIsolated() => new Api(); +} diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 5539d0ea..d6230e8d 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -193,6 +193,13 @@ private async Task ShutdownIfUnusedAsync( return; } + // Clear ownership while still under the write lock — the provider is confirmed unused. + // 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); } @@ -270,6 +277,12 @@ internal async Task ShutdownAsync(Action? 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 { diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj index bc98f8a3..f7851637 100644 --- a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj +++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj @@ -14,6 +14,7 @@ false NU1903 OpenFeature.AotCompatibility + $(NoWarn);OFISO001 @@ -27,8 +28,4 @@ - - - - diff --git a/test/OpenFeature.AotCompatibility/Program.cs b/test/OpenFeature.AotCompatibility/Program.cs index 5529eef2..8c963365 100644 --- a/test/OpenFeature.AotCompatibility/Program.cs +++ b/test/OpenFeature.AotCompatibility/Program.cs @@ -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; @@ -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(); @@ -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..."); diff --git a/test/OpenFeature.Tests/Isolated/IsolatedApiTests.cs b/test/OpenFeature.Tests/Isolated/IsolatedApiTests.cs new file mode 100644 index 00000000..ed90ac5e --- /dev/null +++ b/test/OpenFeature.Tests/Isolated/IsolatedApiTests.cs @@ -0,0 +1,378 @@ +using NSubstitute; +using OpenFeature.Constant; +using OpenFeature.Isolated; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; + +namespace OpenFeature.Tests.Isolated; + +/// +/// Tests for isolated API instances (spec section 1.8). +/// Each test creates its own isolated instances and cleans them up, +/// so no shared state fixture is needed. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(FeatureDiagnosticCodes.IsolatedApi)] +#endif +public class IsolatedApiTests +{ + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void CreateIsolated_Should_Return_New_Instance() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + + Assert.NotNull(isolated); + Assert.NotSame(Api.Instance, isolated); + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void CreateIsolated_Should_Return_Distinct_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + + Assert.NotSame(isolated1, isolated2); + } + + [Fact] + [Specification("1.8.2", "Instances returned by the factory function MUST conform to the same API contract as the global singleton.")] + public async Task Isolated_Instance_Should_Support_Full_Api_Contract() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + // Provider management + var provider = new TestProvider(); + await isolated.SetProviderAsync(provider); + Assert.Equal(provider, isolated.GetProvider()); + Assert.Equal(provider.GetMetadata()?.Name, isolated.GetProviderMetadata()?.Name); + + // Client creation + var client = isolated.GetClient("test-client"); + Assert.NotNull(client); + Assert.Equal("test-client", client.GetMetadata().Name); + + // Hooks + var hook = Substitute.For(); + isolated.AddHooks(hook); + Assert.Contains(hook, isolated.GetHooks()); + + // Context + var context = EvaluationContext.Builder().Set("key", "value").Build(); + isolated.SetContext(context); + Assert.Equal(context, isolated.GetContext()); + + // Event handling (just verify it doesn't throw) + isolated.AddHandler(ProviderEventTypes.ProviderReady, _ => { }); + + // Transaction context + var propagator = Substitute.For(); + isolated.SetTransactionContextPropagator(propagator); + } + finally + { + await isolated.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public async Task Isolated_Provider_Should_Not_Affect_Singleton() + { + Api.ResetApi(); + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("isolated-provider"); + await isolated.SetProviderAsync(provider); + + // The singleton should still have the default NoOp provider + Assert.Equal(NoOpProvider.NoOpProviderName, Api.Instance.GetProviderMetadata()?.Name); + Assert.Equal("isolated-provider", isolated.GetProviderMetadata()?.Name); + } + finally + { + await isolated.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public async Task Isolated_Provider_Should_Not_Affect_Other_Isolated_Instance() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider1 = new TestProvider("provider-1"); + var provider2 = new TestProvider("provider-2"); + await isolated1.SetProviderAsync(provider1); + await isolated2.SetProviderAsync(provider2); + + Assert.Equal("provider-1", isolated1.GetProviderMetadata()?.Name); + Assert.Equal("provider-2", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void Isolated_Hooks_Should_Not_Affect_Other_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + + isolated1.AddHooks(hook1); + isolated2.AddHooks(hook2); + + Assert.Contains(hook1, isolated1.GetHooks()); + Assert.DoesNotContain(hook2, isolated1.GetHooks()); + + Assert.Contains(hook2, isolated2.GetHooks()); + Assert.DoesNotContain(hook1, isolated2.GetHooks()); + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void Isolated_Context_Should_Not_Affect_Other_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + + var context1 = EvaluationContext.Builder().Set("instance", "one").Build(); + var context2 = EvaluationContext.Builder().Set("instance", "two").Build(); + + isolated1.SetContext(context1); + isolated2.SetContext(context2); + + Assert.Equal(context1, isolated1.GetContext()); + Assert.Equal(context2, isolated2.GetContext()); + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Same_Provider_Should_Throw_When_Bound_To_Multiple_Api_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("shared-provider"); + await isolated1.SetProviderAsync(provider); + + var exception = await Assert.ThrowsAsync( + () => isolated2.SetProviderAsync(provider)); + + Assert.Equal("This provider instance is already bound to a different API instance. " + + "A provider should not be registered with more than one API instance simultaneously.", exception.Message); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Same_Provider_Should_Throw_When_Bound_To_Multiple_Api_Instances_Domain() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("shared-provider"); + await isolated1.SetProviderAsync("domain-1", provider); + + var exception = await Assert.ThrowsAsync( + () => isolated2.SetProviderAsync("domain-2", provider)); + + Assert.Equal("This provider instance is already bound to a different API instance. " + + "A provider should not be registered with more than one API instance simultaneously.", exception.Message); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Concurrent_Binding_Should_Allow_Only_One_Api_Instance() + { + const int instanceCount = 10; + var provider = new TestProvider("contested-provider"); + var instances = Enumerable.Range(0, instanceCount).Select(_ => OpenFeatureFactory.CreateIsolated()).ToList(); + try + { + var tasks = instances.Select(api => api.SetProviderAsync(provider)).ToList(); + var results = await Task.WhenAll(tasks.Select(async t => + { + try { await t; return (Exception?)null; } + catch (InvalidOperationException ex) { return ex; } + })); + + var successes = results.Count(r => r is null); + var failures = results.Count(r => r is InvalidOperationException); + + Assert.Equal(1, successes); + Assert.Equal(instanceCount - 1, failures); + } + finally + { + foreach (var api in instances) + { + await api.ShutdownAsync(); + } + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Provider_Can_Be_Rebound_After_Shutdown() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("rebindable-provider"); + await isolated1.SetProviderAsync(provider); + + // Shut down first instance — this frees the provider + await isolated1.ShutdownAsync(); + + // Now the provider should be bindable to another instance + await isolated2.SetProviderAsync(provider); + Assert.Equal("rebindable-provider", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Same_Provider_Can_Be_Bound_To_Multiple_Domains_Within_Same_Api() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("multi-domain-provider"); + await isolated.SetProviderAsync("domain-a", provider); + await isolated.SetProviderAsync("domain-b", provider); + + Assert.Equal(provider, isolated.GetProvider("domain-a")); + Assert.Equal(provider, isolated.GetProvider("domain-b")); + } + finally + { + await isolated.ShutdownAsync(); + } + } + + [Fact] + public async Task Shutdown_Isolated_Should_Not_Affect_Singleton() + { + Api.ResetApi(); + var singletonProvider = new TestProvider("singleton-provider"); + await Api.Instance.SetProviderAsync(singletonProvider); + + var isolated = OpenFeatureFactory.CreateIsolated(); + var isolatedProvider = new TestProvider("isolated-provider"); + await isolated.SetProviderAsync(isolatedProvider); + + // Shutting down isolated instance + await isolated.ShutdownAsync(); + + // Singleton should be unaffected + Assert.Equal("singleton-provider", Api.Instance.GetProviderMetadata()?.Name); + + // Cleanup + await Api.Instance.ShutdownAsync(); + } + + [Fact] + public async Task Shutdown_Isolated_Should_Not_Affect_Other_Isolated() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider1 = new TestProvider("provider-1"); + var provider2 = new TestProvider("provider-2"); + await isolated1.SetProviderAsync(provider1); + await isolated2.SetProviderAsync(provider2); + + await isolated1.ShutdownAsync(); + + // isolated2 should be unaffected + Assert.Equal("provider-2", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated2.ShutdownAsync(); + } + } + + [Fact] + public async Task Provider_Can_Be_Rebound_After_Replacement() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var providerA = new TestProvider("provider-a"); + var providerB = new TestProvider("provider-b"); + + await isolated1.SetProviderAsync(providerA); + + // Replace providerA with providerB in isolated1 — providerA should be unbound + await isolated1.SetProviderAsync(providerB); + + // Allow some time for the async shutdown to complete + await Task.Delay(100); + + // providerA should now be free to bind to isolated2 + await isolated2.SetProviderAsync(providerA); + Assert.Equal("provider-a", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.2", "Instances returned by the factory function MUST conform to the same API contract as the global singleton.")] + public async Task Isolated_Client_Should_Evaluate_Flags() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("eval-provider"); + await isolated.SetProviderAsync(provider); + + var client = isolated.GetClient(); + + // TestProvider inverts boolean defaults + var result = await client.GetBooleanValueAsync("test-flag", false); + Assert.True(result); + } + finally + { + await isolated.ShutdownAsync(); + } + } +}