Skip to content

Commit 8cb5f6b

Browse files
committed
Feat: Add support for provider hooks
- [Breaking] Remove IFeatureProvider interface infavor of FeatureProvider abstract class so default implementations can exist - Make sure Provider hooks are called in the correct order - Use strick mocking mode so sequence is validated correctly - Update test cases for provider hooks support Signed-off-by: Benjamin Evenson <[email protected]>
1 parent aae53c6 commit 8cb5f6b

11 files changed

+268
-126
lines changed

src/OpenFeature.SDK/IFeatureProvider.cs src/OpenFeature.SDK/FeatureProvider.cs

+22-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System;
2+
using System.Collections.Generic;
13
using System.Threading.Tasks;
24
using OpenFeature.SDK.Model;
35

@@ -8,13 +10,26 @@ namespace OpenFeature.SDK
810
/// A provider acts as the translates layer between the generic feature flag structure to a target feature flag system.
911
/// </summary>
1012
/// <seealso href="https://github.com/open-feature/spec/blob/main/specification/providers.md">Provider specification</seealso>
11-
public interface IFeatureProvider
13+
public abstract class FeatureProvider
1214
{
15+
/// <summary>
16+
/// Gets a immutable list of hooks that belong to the provider.
17+
/// By default return a empty list
18+
///
19+
/// Executed in the order of hooks
20+
/// before: API, Client, Invocation, Provider
21+
/// after: Provider, Invocation, Client, API
22+
/// error (if applicable): Provider, Invocation, Client, API
23+
/// finally: Provider, Invocation, Client, API
24+
/// </summary>
25+
/// <returns></returns>
26+
public virtual IReadOnlyList<Hook> GetProviderHooks() => Array.Empty<Hook>();
27+
1328
/// <summary>
1429
/// Metadata describing the provider.
1530
/// </summary>
1631
/// <returns><see cref="Metadata"/></returns>
17-
Metadata GetMetadata();
32+
public abstract Metadata GetMetadata();
1833

1934
/// <summary>
2035
/// Resolves a boolean feature flag
@@ -24,7 +39,7 @@ public interface IFeatureProvider
2439
/// <param name="context"><see cref="EvaluationContext"/></param>
2540
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
2641
/// <returns><see cref="ResolutionDetails{T}"/></returns>
27-
Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue,
42+
public abstract Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue,
2843
EvaluationContext context = null, FlagEvaluationOptions config = null);
2944

3045
/// <summary>
@@ -35,7 +50,7 @@ Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultVa
3550
/// <param name="context"><see cref="EvaluationContext"/></param>
3651
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
3752
/// <returns><see cref="ResolutionDetails{T}"/></returns>
38-
Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue,
53+
public abstract Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue,
3954
EvaluationContext context = null, FlagEvaluationOptions config = null);
4055

4156
/// <summary>
@@ -46,7 +61,7 @@ Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaul
4661
/// <param name="context"><see cref="EvaluationContext"/></param>
4762
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
4863
/// <returns><see cref="ResolutionDetails{T}"/></returns>
49-
Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue,
64+
public abstract Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue,
5065
EvaluationContext context = null, FlagEvaluationOptions config = null);
5166

5267
/// <summary>
@@ -57,7 +72,7 @@ Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValu
5772
/// <param name="context"><see cref="EvaluationContext"/></param>
5873
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
5974
/// <returns><see cref="ResolutionDetails{T}"/></returns>
60-
Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue,
75+
public abstract Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue,
6176
EvaluationContext context = null, FlagEvaluationOptions config = null);
6277

6378
/// <summary>
@@ -69,7 +84,7 @@ Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaul
6984
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
7085
/// <typeparam name="T">Type of object</typeparam>
7186
/// <returns><see cref="ResolutionDetails{T}"/></returns>
72-
Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue,
87+
public abstract Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue,
7388
EvaluationContext context = null, FlagEvaluationOptions config = null);
7489
}
7590
}

src/OpenFeature.SDK/Model/ResolutionDetails.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace OpenFeature.SDK.Model
44
{
55
/// <summary>
6-
/// Defines the contract that the <see cref="IFeatureProvider"/> is required to return
6+
/// Defines the contract that the <see cref="FeatureProvider"/> is required to return
77
/// Describes the details of the feature flag being evaluated
88
/// </summary>
99
/// <typeparam name="T">Flag value type</typeparam>

src/OpenFeature.SDK/NoOpProvider.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,37 @@
44

55
namespace OpenFeature.SDK
66
{
7-
internal class NoOpFeatureProvider : IFeatureProvider
7+
internal class NoOpFeatureProvider : FeatureProvider
88
{
99
private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName);
1010

11-
public Metadata GetMetadata()
11+
public override Metadata GetMetadata()
1212
{
1313
return this._metadata;
1414
}
1515

16-
public Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
16+
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
1717
{
1818
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
1919
}
2020

21-
public Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
21+
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
2222
{
2323
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
2424
}
2525

26-
public Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
26+
public override Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
2727
{
2828
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
2929
}
3030

31-
public Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null,
31+
public override Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null,
3232
FlagEvaluationOptions config = null)
3333
{
3434
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
3535
}
3636

37-
public Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
37+
public override Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
3838
{
3939
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
4040
}

src/OpenFeature.SDK/OpenFeature.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace OpenFeature.SDK
1212
public sealed class OpenFeature
1313
{
1414
private EvaluationContext _evaluationContext = new EvaluationContext();
15-
private IFeatureProvider _featureProvider = new NoOpFeatureProvider();
15+
private FeatureProvider _featureProvider = new NoOpFeatureProvider();
1616
private readonly List<Hook> _hooks = new List<Hook>();
1717

1818
/// <summary>
@@ -29,14 +29,14 @@ private OpenFeature() { }
2929
/// <summary>
3030
/// Sets the feature provider
3131
/// </summary>
32-
/// <param name="featureProvider">Implementation of <see cref="IFeatureProvider"/></param>
33-
public void SetProvider(IFeatureProvider featureProvider) => this._featureProvider = featureProvider;
32+
/// <param name="featureProvider">Implementation of <see cref="FeatureProvider"/></param>
33+
public void SetProvider(FeatureProvider featureProvider) => this._featureProvider = featureProvider;
3434

3535
/// <summary>
3636
/// Gets the feature provider
3737
/// </summary>
38-
/// <returns><see cref="IFeatureProvider"/></returns>
39-
public IFeatureProvider GetProvider() => this._featureProvider;
38+
/// <returns><see cref="FeatureProvider"/></returns>
39+
public FeatureProvider GetProvider() => this._featureProvider;
4040

4141
/// <summary>
4242
/// Gets providers metadata
@@ -81,7 +81,7 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge
8181
/// Sets the global <see cref="EvaluationContext"/>
8282
/// </summary>
8383
/// <param name="context"></param>
84-
public void SetContext(EvaluationContext context) => this._evaluationContext = context;
84+
public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? new EvaluationContext();
8585

8686
/// <summary>
8787
/// Gets the global <see cref="EvaluationContext"/>

src/OpenFeature.SDK/OpenFeatureClient.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ namespace OpenFeature.SDK
1717
public sealed class FeatureClient : IFeatureClient
1818
{
1919
private readonly ClientMetadata _metadata;
20-
private readonly IFeatureProvider _featureProvider;
20+
private readonly FeatureProvider _featureProvider;
2121
private readonly List<Hook> _hooks = new List<Hook>();
2222
private readonly ILogger _logger;
2323

2424
/// <summary>
2525
/// Initializes a new instance of the <see cref="FeatureClient"/> class.
2626
/// </summary>
27-
/// <param name="featureProvider">Feature provider used by client <see cref="IFeatureProvider"/></param>
27+
/// <param name="featureProvider">Feature provider used by client <see cref="FeatureProvider"/></param>
2828
/// <param name="name">Name of client <see cref="ClientMetadata"/></param>
2929
/// <param name="version">Version of client <see cref="ClientMetadata"/></param>
3030
/// <param name="logger">Logger used by client</param>
3131
/// <exception cref="ArgumentNullException">Throws if any of the required parameters are null</exception>
32-
public FeatureClient(IFeatureProvider featureProvider, string name, string version, ILogger logger = null)
32+
public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null)
3333
{
3434
this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider));
3535
this._metadata = new ClientMetadata(name, version);
@@ -209,6 +209,7 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlag<T>(
209209
.Concat(OpenFeature.Instance.GetHooks())
210210
.Concat(this._hooks)
211211
.Concat(options?.Hooks ?? Enumerable.Empty<Hook>())
212+
.Concat(this._featureProvider.GetProviderHooks())
212213
.ToList()
213214
.AsReadOnly();
214215

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace OpenFeature.SDK.Tests
2+
{
3+
public class ClearOpenFeatureInstanceFixture
4+
{
5+
// Make sure the singleton is cleared between tests
6+
public ClearOpenFeatureInstanceFixture()
7+
{
8+
OpenFeature.Instance.SetContext(null);
9+
OpenFeature.Instance.ClearHooks();
10+
OpenFeature.Instance.SetProvider(new NoOpFeatureProvider());
11+
}
12+
}
13+
}

test/OpenFeature.SDK.Tests/FeatureProviderTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace OpenFeature.SDK.Tests
1111
{
12-
public class FeatureProviderTests
12+
public class FeatureProviderTests : ClearOpenFeatureInstanceFixture
1313
{
1414
[Fact]
1515
[Specification("2.1", "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")]
@@ -67,7 +67,7 @@ public async Task Provider_Must_ErrorType()
6767
var defaultIntegerValue = fixture.Create<int>();
6868
var defaultDoubleValue = fixture.Create<double>();
6969
var defaultStructureValue = fixture.Create<TestStructure>();
70-
var providerMock = new Mock<IFeatureProvider>();
70+
var providerMock = new Mock<FeatureProvider>(MockBehavior.Strict);
7171

7272
providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny<EvaluationContext>(), It.IsAny<FlagEvaluationOptions>()))
7373
.ReturnsAsync(new ResolutionDetails<bool>(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant));

0 commit comments

Comments
 (0)