diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index ba3fea3b..e679bf75 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -15,7 +15,9 @@ namespace OpenFeature public sealed class Api { private EvaluationContext _evaluationContext = EvaluationContext.Empty; - private FeatureProvider _featureProvider = new NoOpFeatureProvider(); + private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); + private readonly ConcurrentDictionary _featureProviders = + new ConcurrentDictionary(); private readonly ConcurrentStack _hooks = new ConcurrentStack(); /// The reader/writer locks are not disposed because the singleton instance should never be disposed. @@ -42,7 +44,7 @@ public void SetProvider(FeatureProvider featureProvider) this._featureProviderLock.EnterWriteLock(); try { - this._featureProvider = featureProvider; + this._defaultProvider = featureProvider ?? this._defaultProvider; } finally { @@ -50,6 +52,17 @@ public void SetProvider(FeatureProvider featureProvider) } } + /// + /// Sets the feature provider to given clientName + /// + /// Name of client + /// Implementation of + public void SetProvider(string clientName, FeatureProvider featureProvider) + { + this._featureProviders.AddOrUpdate(clientName, featureProvider, + (key, current) => featureProvider); + } + /// /// Gets the feature provider /// @@ -57,7 +70,7 @@ public void SetProvider(FeatureProvider featureProvider) /// it should be accessed once for an operation, and then that reference should be used for all dependent /// operations. For instance, during an evaluation the flag resolution method, and the provider hooks /// should be accessed from the same reference, not two independent calls to - /// . + /// . /// /// /// @@ -66,7 +79,7 @@ public FeatureProvider GetProvider() this._featureProviderLock.EnterReadLock(); try { - return this._featureProvider; + return this._defaultProvider; } finally { @@ -74,17 +87,44 @@ public FeatureProvider GetProvider() } } + /// + /// Gets the feature provider with given clientName + /// + /// Name of client + /// A provider associated with the given clientName, if clientName is empty or doesn't + /// have a corresponding provider the default provider will be returned + public FeatureProvider GetProvider(string clientName) + { + if (string.IsNullOrEmpty(clientName)) + { + return this.GetProvider(); + } + + return this._featureProviders.TryGetValue(clientName, out var featureProvider) + ? featureProvider + : this.GetProvider(); + } + + /// /// Gets providers metadata /// /// This method is not guaranteed to return the same provider instance that may be used during an evaluation /// in the case where the provider may be changed from another thread. - /// For multiple dependent provider operations see . + /// For multiple dependent provider operations see . /// /// /// public Metadata GetProviderMetadata() => this.GetProvider().GetMetadata(); + /// + /// Gets providers metadata assigned to the given clientName. If the clientName has no provider + /// assigned to it the default provider will be returned + /// + /// Name of client + /// Metadata assigned to provider + public Metadata GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); + /// /// Create a new instance of using the current provider /// diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 115f7510..38a0fd63 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -42,13 +42,7 @@ public sealed class FeatureClient : IFeatureClient { // Alias the provider reference so getting the method and returning the provider are // guaranteed to be the same object. - var provider = Api.Instance.GetProvider(); - - if (provider == null) - { - provider = new NoOpFeatureProvider(); - this._logger.LogDebug("No provider configured, using no-op provider"); - } + var provider = Api.Instance.GetProvider(this._metadata.Name); return (method(provider), provider); } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index b5249f6b..aec5a631 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -147,7 +147,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() } [Fact] - [Specification("1.1.2", "The `API` MUST provide a function to set the global `provider` singleton, which accepts an API-conformant `provider` implementation.")] + [Specification("1.1.2", "The `API` MUST provide a function to set the default `provider`, which accepts an API-conformant `provider` implementation.")] [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] [Specification("1.4.7", "In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST contain an `error code`.")] [Specification("1.4.8", "In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")] diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 5292fa82..f6ad03a0 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -21,7 +21,65 @@ public void OpenFeature_Should_Be_Singleton() } [Fact] - [Specification("1.1.3", "The `API` MUST provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider() + { + var openFeature = Api.Instance; + + openFeature.SetProvider(new NoOpFeatureProvider()); + openFeature.SetProvider(TestProvider.Name, new TestProvider()); + + var defaultClient = openFeature.GetProviderMetadata(); + var namedClient = openFeature.GetProviderMetadata(TestProvider.Name); + + defaultClient.Name.Should().Be(NoOpProvider.NoOpProviderName); + namedClient.Name.Should().Be(TestProvider.Name); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() + { + var openFeature = Api.Instance; + + openFeature.SetProvider(new TestProvider()); + + var defaultClient = openFeature.GetProviderMetadata(); + + defaultClient.Name.Should().Be(TestProvider.Name); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Assign_Provider_To_Existing_Client() + { + const string name = "new-client"; + var openFeature = Api.Instance; + + openFeature.SetProvider(name, new TestProvider()); + openFeature.SetProvider(name, new NoOpFeatureProvider()); + + openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance() + { + var openFeature = Api.Instance; + var provider = new TestProvider(); + + openFeature.SetProvider("a", provider); + openFeature.SetProvider("b", provider); + + var clientA = openFeature.GetProvider("a"); + var clientB = openFeature.GetProvider("b"); + + clientA.Should().Be(clientB); + } + + [Fact] + [Specification("1.1.4", "The `API` MUST provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] public void OpenFeature_Should_Add_Hooks() { var openFeature = Api.Instance; @@ -50,7 +108,7 @@ public void OpenFeature_Should_Add_Hooks() } [Fact] - [Specification("1.1.4", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] + [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] public void OpenFeature_Should_Get_Metadata() { Api.Instance.SetProvider(new NoOpFeatureProvider()); @@ -65,7 +123,7 @@ public void OpenFeature_Should_Get_Metadata() [InlineData("client1", "version1")] [InlineData("client2", null)] [InlineData(null, null)] - [Specification("1.1.5", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] + [Specification("1.1.6", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] public void OpenFeature_Should_Create_Client(string name = null, string version = null) { var openFeature = Api.Instance; @@ -97,5 +155,23 @@ public void Should_Always_Have_Provider() { Api.Instance.GetProvider().Should().NotBeNull(); } + + [Fact] + public void OpenFeature_Should_Allow_Multiple_Client_Mapping() + { + var openFeature = Api.Instance; + + openFeature.SetProvider("client1", new TestProvider()); + openFeature.SetProvider("client2", new NoOpFeatureProvider()); + + var client1 = openFeature.GetClient("client1"); + var client2 = openFeature.GetClient("client2"); + + client1.GetMetadata().Name.Should().Be("client1"); + client2.GetMetadata().Name.Should().Be("client2"); + + client1.GetBooleanValue("test", false).Result.Should().BeTrue(); + client2.GetBooleanValue("test", false).Result.Should().BeFalse(); + } } } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index b54462f3..e2bcf5e9 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -50,7 +50,7 @@ public override Metadata GetMetadata() public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); } public override Task> ResolveStringValue(string flagKey, string defaultValue,