Skip to content

Commit 3f765c6

Browse files
authoredMay 31, 2023
feat: Support for name client to given provider (#129)
Signed-off-by: Benjamin Evenson <[email protected]>
1 parent 9152d63 commit 3f765c6

File tree

5 files changed

+127
-17
lines changed

5 files changed

+127
-17
lines changed
 

‎src/OpenFeature/Api.cs

+45-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ namespace OpenFeature
1515
public sealed class Api
1616
{
1717
private EvaluationContext _evaluationContext = EvaluationContext.Empty;
18-
private FeatureProvider _featureProvider = new NoOpFeatureProvider();
18+
private FeatureProvider _defaultProvider = new NoOpFeatureProvider();
19+
private readonly ConcurrentDictionary<string, FeatureProvider> _featureProviders =
20+
new ConcurrentDictionary<string, FeatureProvider>();
1921
private readonly ConcurrentStack<Hook> _hooks = new ConcurrentStack<Hook>();
2022

2123
/// The reader/writer locks are not disposed because the singleton instance should never be disposed.
@@ -42,22 +44,33 @@ public void SetProvider(FeatureProvider featureProvider)
4244
this._featureProviderLock.EnterWriteLock();
4345
try
4446
{
45-
this._featureProvider = featureProvider;
47+
this._defaultProvider = featureProvider ?? this._defaultProvider;
4648
}
4749
finally
4850
{
4951
this._featureProviderLock.ExitWriteLock();
5052
}
5153
}
5254

55+
/// <summary>
56+
/// Sets the feature provider to given clientName
57+
/// </summary>
58+
/// <param name="clientName">Name of client</param>
59+
/// <param name="featureProvider">Implementation of <see cref="FeatureProvider"/></param>
60+
public void SetProvider(string clientName, FeatureProvider featureProvider)
61+
{
62+
this._featureProviders.AddOrUpdate(clientName, featureProvider,
63+
(key, current) => featureProvider);
64+
}
65+
5366
/// <summary>
5467
/// Gets the feature provider
5568
/// <para>
5669
/// The feature provider may be set from multiple threads, when accessing the global feature provider
5770
/// it should be accessed once for an operation, and then that reference should be used for all dependent
5871
/// operations. For instance, during an evaluation the flag resolution method, and the provider hooks
5972
/// should be accessed from the same reference, not two independent calls to
60-
/// <see cref="GetProvider"/>.
73+
/// <see cref="GetProvider()"/>.
6174
/// </para>
6275
/// </summary>
6376
/// <returns><see cref="FeatureProvider"/></returns>
@@ -66,25 +79,52 @@ public FeatureProvider GetProvider()
6679
this._featureProviderLock.EnterReadLock();
6780
try
6881
{
69-
return this._featureProvider;
82+
return this._defaultProvider;
7083
}
7184
finally
7285
{
7386
this._featureProviderLock.ExitReadLock();
7487
}
7588
}
7689

90+
/// <summary>
91+
/// Gets the feature provider with given clientName
92+
/// </summary>
93+
/// <param name="clientName">Name of client</param>
94+
/// <returns>A provider associated with the given clientName, if clientName is empty or doesn't
95+
/// have a corresponding provider the default provider will be returned</returns>
96+
public FeatureProvider GetProvider(string clientName)
97+
{
98+
if (string.IsNullOrEmpty(clientName))
99+
{
100+
return this.GetProvider();
101+
}
102+
103+
return this._featureProviders.TryGetValue(clientName, out var featureProvider)
104+
? featureProvider
105+
: this.GetProvider();
106+
}
107+
108+
77109
/// <summary>
78110
/// Gets providers metadata
79111
/// <para>
80112
/// This method is not guaranteed to return the same provider instance that may be used during an evaluation
81113
/// in the case where the provider may be changed from another thread.
82-
/// For multiple dependent provider operations see <see cref="GetProvider"/>.
114+
/// For multiple dependent provider operations see <see cref="GetProvider()"/>.
83115
/// </para>
84116
/// </summary>
85117
/// <returns><see cref="ClientMetadata"/></returns>
86118
public Metadata GetProviderMetadata() => this.GetProvider().GetMetadata();
87119

120+
/// <summary>
121+
/// Gets providers metadata assigned to the given clientName. If the clientName has no provider
122+
/// assigned to it the default provider will be returned
123+
/// </summary>
124+
/// <param name="clientName">Name of client</param>
125+
/// <returns>Metadata assigned to provider</returns>
126+
public Metadata GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata();
127+
88128
/// <summary>
89129
/// Create a new instance of <see cref="FeatureClient"/> using the current provider
90130
/// </summary>

‎src/OpenFeature/OpenFeatureClient.cs

+1-7
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,7 @@ public sealed class FeatureClient : IFeatureClient
4242
{
4343
// Alias the provider reference so getting the method and returning the provider are
4444
// guaranteed to be the same object.
45-
var provider = Api.Instance.GetProvider();
46-
47-
if (provider == null)
48-
{
49-
provider = new NoOpFeatureProvider();
50-
this._logger.LogDebug("No provider configured, using no-op provider");
51-
}
45+
var provider = Api.Instance.GetProvider(this._metadata.Name);
5246

5347
return (method(provider), provider);
5448
}

‎test/OpenFeature.Tests/OpenFeatureClientTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation()
147147
}
148148

149149
[Fact]
150-
[Specification("1.1.2", "The `API` MUST provide a function to set the global `provider` singleton, which accepts an API-conformant `provider` implementation.")]
150+
[Specification("1.1.2", "The `API` MUST provide a function to set the default `provider`, which accepts an API-conformant `provider` implementation.")]
151151
[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.")]
152152
[Specification("1.4.7", "In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST contain an `error code`.")]
153153
[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.")]

‎test/OpenFeature.Tests/OpenFeatureTests.cs

+79-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,65 @@ public void OpenFeature_Should_Be_Singleton()
2121
}
2222

2323
[Fact]
24-
[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.")]
24+
[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.")]
25+
public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider()
26+
{
27+
var openFeature = Api.Instance;
28+
29+
openFeature.SetProvider(new NoOpFeatureProvider());
30+
openFeature.SetProvider(TestProvider.Name, new TestProvider());
31+
32+
var defaultClient = openFeature.GetProviderMetadata();
33+
var namedClient = openFeature.GetProviderMetadata(TestProvider.Name);
34+
35+
defaultClient.Name.Should().Be(NoOpProvider.NoOpProviderName);
36+
namedClient.Name.Should().Be(TestProvider.Name);
37+
}
38+
39+
[Fact]
40+
[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.")]
41+
public void OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided()
42+
{
43+
var openFeature = Api.Instance;
44+
45+
openFeature.SetProvider(new TestProvider());
46+
47+
var defaultClient = openFeature.GetProviderMetadata();
48+
49+
defaultClient.Name.Should().Be(TestProvider.Name);
50+
}
51+
52+
[Fact]
53+
[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.")]
54+
public void OpenFeature_Should_Assign_Provider_To_Existing_Client()
55+
{
56+
const string name = "new-client";
57+
var openFeature = Api.Instance;
58+
59+
openFeature.SetProvider(name, new TestProvider());
60+
openFeature.SetProvider(name, new NoOpFeatureProvider());
61+
62+
openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName);
63+
}
64+
65+
[Fact]
66+
[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.")]
67+
public void OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance()
68+
{
69+
var openFeature = Api.Instance;
70+
var provider = new TestProvider();
71+
72+
openFeature.SetProvider("a", provider);
73+
openFeature.SetProvider("b", provider);
74+
75+
var clientA = openFeature.GetProvider("a");
76+
var clientB = openFeature.GetProvider("b");
77+
78+
clientA.Should().Be(clientB);
79+
}
80+
81+
[Fact]
82+
[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.")]
2583
public void OpenFeature_Should_Add_Hooks()
2684
{
2785
var openFeature = Api.Instance;
@@ -50,7 +108,7 @@ public void OpenFeature_Should_Add_Hooks()
50108
}
51109

52110
[Fact]
53-
[Specification("1.1.4", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")]
111+
[Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")]
54112
public void OpenFeature_Should_Get_Metadata()
55113
{
56114
Api.Instance.SetProvider(new NoOpFeatureProvider());
@@ -65,7 +123,7 @@ public void OpenFeature_Should_Get_Metadata()
65123
[InlineData("client1", "version1")]
66124
[InlineData("client2", null)]
67125
[InlineData(null, null)]
68-
[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.")]
126+
[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.")]
69127
public void OpenFeature_Should_Create_Client(string name = null, string version = null)
70128
{
71129
var openFeature = Api.Instance;
@@ -97,5 +155,23 @@ public void Should_Always_Have_Provider()
97155
{
98156
Api.Instance.GetProvider().Should().NotBeNull();
99157
}
158+
159+
[Fact]
160+
public void OpenFeature_Should_Allow_Multiple_Client_Mapping()
161+
{
162+
var openFeature = Api.Instance;
163+
164+
openFeature.SetProvider("client1", new TestProvider());
165+
openFeature.SetProvider("client2", new NoOpFeatureProvider());
166+
167+
var client1 = openFeature.GetClient("client1");
168+
var client2 = openFeature.GetClient("client2");
169+
170+
client1.GetMetadata().Name.Should().Be("client1");
171+
client2.GetMetadata().Name.Should().Be("client2");
172+
173+
client1.GetBooleanValue("test", false).Result.Should().BeTrue();
174+
client2.GetBooleanValue("test", false).Result.Should().BeFalse();
175+
}
100176
}
101177
}

‎test/OpenFeature.Tests/TestImplementations.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public override Metadata GetMetadata()
5050
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue,
5151
EvaluationContext context = null)
5252
{
53-
return Task.FromResult(new ResolutionDetails<bool>(flagKey, defaultValue));
53+
return Task.FromResult(new ResolutionDetails<bool>(flagKey, !defaultValue));
5454
}
5555

5656
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue,

0 commit comments

Comments
 (0)
Please sign in to comment.