diff --git a/src/OpenFeatureSDK/FeatureProvider.cs b/src/OpenFeatureSDK/FeatureProvider.cs
index 639363a8..e2934cea 100644
--- a/src/OpenFeatureSDK/FeatureProvider.cs
+++ b/src/OpenFeatureSDK/FeatureProvider.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Threading.Tasks;
using OpenFeatureSDK.Model;
@@ -22,8 +21,8 @@ public abstract class FeatureProvider
/// error (if applicable): Provider, Invocation, Client, API
/// finally: Provider, Invocation, Client, API
///
- ///
- public virtual IReadOnlyList GetProviderHooks() => Array.Empty();
+ /// Immutable list of hooks
+ public virtual IImmutableList GetProviderHooks() => ImmutableList.Empty;
///
/// Metadata describing the provider.
diff --git a/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs b/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs
index cb8bc353..38396723 100644
--- a/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs
+++ b/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Immutable;
namespace OpenFeatureSDK.Model
{
@@ -12,33 +12,33 @@ public class FlagEvaluationOptions
///
/// A immutable list of
///
- public IReadOnlyList Hooks { get; }
+ public IImmutableList Hooks { get; }
///
/// A immutable dictionary of hook hints
///
- public IReadOnlyDictionary HookHints { get; }
+ public IImmutableDictionary HookHints { get; }
///
/// Initializes a new instance of the class.
///
- ///
+ /// An immutable list of hooks to use during evaluation
/// Optional - a list of hints that are passed through the hook lifecycle
- public FlagEvaluationOptions(IReadOnlyList hooks, IReadOnlyDictionary hookHints = null)
+ public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary hookHints = null)
{
this.Hooks = hooks;
- this.HookHints = hookHints ?? new Dictionary();
+ this.HookHints = hookHints ?? ImmutableDictionary.Empty;
}
///
/// Initializes a new instance of the class.
///
- ///
+ /// A hook to use during the evaluation
/// Optional - a list of hints that are passed through the hook lifecycle
- public FlagEvaluationOptions(Hook hook, IReadOnlyDictionary hookHints = null)
+ public FlagEvaluationOptions(Hook hook, ImmutableDictionary hookHints = null)
{
- this.Hooks = new[] { hook };
- this.HookHints = hookHints ?? new Dictionary();
+ this.Hooks = ImmutableList.Create(hook);
+ this.HookHints = hookHints ?? ImmutableDictionary.Empty;
}
}
}
diff --git a/src/OpenFeatureSDK/OpenFeature.cs b/src/OpenFeatureSDK/OpenFeature.cs
index 42b23dcf..cf119f9f 100644
--- a/src/OpenFeatureSDK/OpenFeature.cs
+++ b/src/OpenFeatureSDK/OpenFeature.cs
@@ -1,4 +1,8 @@
+using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
using Microsoft.Extensions.Logging;
using OpenFeatureSDK.Model;
@@ -13,7 +17,11 @@ public sealed class OpenFeature
{
private EvaluationContext _evaluationContext = EvaluationContext.Empty;
private FeatureProvider _featureProvider = new NoOpFeatureProvider();
- private readonly List _hooks = new List();
+ private readonly ConcurrentStack _hooks = new ConcurrentStack();
+
+ /// The reader/writer locks are not disposed because the singleton instance should never be disposed.
+ private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim();
+ private readonly ReaderWriterLockSlim _featureProviderLock = new ReaderWriterLockSlim();
///
/// Singleton instance of OpenFeature
@@ -30,19 +38,53 @@ private OpenFeature() { }
/// Sets the feature provider
///
/// Implementation of
- public void SetProvider(FeatureProvider featureProvider) => this._featureProvider = featureProvider;
+ public void SetProvider(FeatureProvider featureProvider)
+ {
+ this._featureProviderLock.EnterWriteLock();
+ try
+ {
+ this._featureProvider = featureProvider;
+ }
+ finally
+ {
+ this._featureProviderLock.ExitWriteLock();
+ }
+ }
///
/// Gets the feature provider
+ ///
+ /// The feature provider may be set from multiple threads, when accessing the global feature provider
+ /// 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
+ /// .
+ ///
///
///
- public FeatureProvider GetProvider() => this._featureProvider;
+ public FeatureProvider GetProvider()
+ {
+ this._featureProviderLock.EnterReadLock();
+ try
+ {
+ return this._featureProvider;
+ }
+ finally
+ {
+ this._featureProviderLock.ExitReadLock();
+ }
+ }
///
/// 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 .
+ ///
///
///
- public Metadata GetProviderMetadata() => this._featureProvider.GetMetadata();
+ public Metadata GetProviderMetadata() => this.GetProvider().GetMetadata();
///
/// Create a new instance of using the current provider
@@ -52,26 +94,39 @@ private OpenFeature() { }
/// Logger instance used by client
/// Context given to this client
///
- public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null, EvaluationContext context = null) =>
- new FeatureClient(this._featureProvider, name, version, logger, context);
+ public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null,
+ EvaluationContext context = null) =>
+ new FeatureClient(name, version, logger, context);
///
/// Appends list of hooks to global hooks list
+ ///
+ /// The appending operation will be atomic.
+ ///
///
/// A list of
- public void AddHooks(IEnumerable hooks) => this._hooks.AddRange(hooks);
+ public void AddHooks(IEnumerable hooks) => this._hooks.PushRange(hooks.ToArray());
///
/// Adds a hook to global hooks list
+ ///
+ /// Hooks which are dependent on each other should be provided in a collection
+ /// using the .
+ ///
///
- /// A list of
- public void AddHooks(Hook hook) => this._hooks.Add(hook);
+ /// Hook that implements the interface
+ public void AddHooks(Hook hook) => this._hooks.Push(hook);
///
- /// Returns the global immutable hooks list
+ /// Enumerates the global hooks.
+ ///
+ /// The items enumerated will reflect the registered hooks
+ /// at the start of enumeration. Hooks added during enumeration
+ /// will not be included.
+ ///
///
- /// A immutable list of
- public IReadOnlyList GetHooks() => this._hooks.AsReadOnly();
+ /// Enumeration of
+ public IEnumerable GetHooks() => this._hooks.Reverse();
///
/// Removes all hooks from global hooks list
@@ -81,13 +136,40 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge
///
/// Sets the global
///
- ///
- public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? EvaluationContext.Empty;
+ /// The to set
+ public void SetContext(EvaluationContext context)
+ {
+ this._evaluationContextLock.EnterWriteLock();
+ try
+ {
+ this._evaluationContext = context ?? EvaluationContext.Empty;
+ }
+ finally
+ {
+ this._evaluationContextLock.ExitWriteLock();
+ }
+ }
///
/// Gets the global
+ ///
+ /// The evaluation context may be set from multiple threads, when accessing the global evaluation context
+ /// it should be accessed once for an operation, and then that reference should be used for all dependent
+ /// operations.
+ ///
///
- ///
- public EvaluationContext GetContext() => this._evaluationContext;
+ /// An
+ public EvaluationContext GetContext()
+ {
+ this._evaluationContextLock.EnterReadLock();
+ try
+ {
+ return this._evaluationContext;
+ }
+ finally
+ {
+ this._evaluationContextLock.ExitReadLock();
+ }
+ }
}
}
diff --git a/src/OpenFeatureSDK/OpenFeatureClient.cs b/src/OpenFeatureSDK/OpenFeatureClient.cs
index 3d4120b7..efe47cb6 100644
--- a/src/OpenFeatureSDK/OpenFeatureClient.cs
+++ b/src/OpenFeatureSDK/OpenFeatureClient.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -17,34 +18,80 @@ namespace OpenFeatureSDK
public sealed class FeatureClient : IFeatureClient
{
private readonly ClientMetadata _metadata;
- private readonly FeatureProvider _featureProvider;
- private readonly List _hooks = new List();
+ private readonly ConcurrentStack _hooks = new ConcurrentStack();
private readonly ILogger _logger;
private EvaluationContext _evaluationContext;
+ private readonly object _evaluationContextLock = new object();
+
+ ///
+ /// Get a provider and an associated typed flag resolution method.
+ ///
+ /// The global provider could change between two accesses, so in order to safely get provider information we
+ /// must first alias it and then use that alias to access everything we need.
+ ///
+ ///
+ ///
+ /// This method should return the desired flag resolution method from the given provider reference.
+ ///
+ /// The type of the resolution method
+ /// A tuple containing a resolution method and the provider it came from.
+ private (Func>>, FeatureProvider)
+ ExtractProvider(
+ Func>>> method)
+ {
+ // Alias the provider reference so getting the method and returning the provider are
+ // guaranteed to be the same object.
+ var provider = OpenFeature.Instance.GetProvider();
+
+ if (provider == null)
+ {
+ provider = new NoOpFeatureProvider();
+ this._logger.LogDebug("No provider configured, using no-op provider");
+ }
+
+ return (method(provider), provider);
+ }
+
///
/// Gets the EvaluationContext of this client
+ ///
+ /// The evaluation context may be set from multiple threads, when accessing the client evaluation context
+ /// it should be accessed once for an operation, and then that reference should be used for all dependent
+ /// operations.
+ ///
///
/// of this client
- public EvaluationContext GetContext() => this._evaluationContext;
+ public EvaluationContext GetContext()
+ {
+ lock (this._evaluationContextLock)
+ {
+ return this._evaluationContext;
+ }
+ }
///
/// Sets the EvaluationContext of the client
///
- public void SetContext(EvaluationContext evaluationContext) => this._evaluationContext = evaluationContext;
+ /// The to set
+ public void SetContext(EvaluationContext context)
+ {
+ lock (this._evaluationContextLock)
+ {
+ this._evaluationContext = context ?? EvaluationContext.Empty;
+ }
+ }
///
/// Initializes a new instance of the class.
///
- /// Feature provider used by client
/// Name of client
/// Version of client
/// Logger used by client
/// Context given to this client
/// Throws if any of the required parameters are null
- public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null, EvaluationContext context = null)
+ public FeatureClient(string name, string version, ILogger logger = null, EvaluationContext context = null)
{
- this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider));
this._metadata = new ClientMetadata(name, version);
this._logger = logger ?? new Logger(new NullLoggerFactory());
this._evaluationContext = context ?? EvaluationContext.Empty;
@@ -58,21 +105,33 @@ public FeatureClient(FeatureProvider featureProvider, string name, string versio
///
/// Add hook to client
+ ///
+ /// Hooks which are dependent on each other should be provided in a collection
+ /// using the .
+ ///
///
/// Hook that implements the interface
- public void AddHooks(Hook hook) => this._hooks.Add(hook);
+ public void AddHooks(Hook hook) => this._hooks.Push(hook);
///
/// Appends hooks to client
+ ///
+ /// The appending operation will be atomic.
+ ///
///
/// A list of Hooks that implement the interface
- public void AddHooks(IEnumerable hooks) => this._hooks.AddRange(hooks);
+ public void AddHooks(IEnumerable hooks) => this._hooks.PushRange(hooks.ToArray());
///
- /// Return a immutable list of hooks that are registered against the client
+ /// Enumerates the global hooks.
+ ///
+ /// The items enumerated will reflect the registered hooks
+ /// at the start of enumeration. Hooks added during enumeration
+ /// will not be included.
+ ///
///
- /// A list of immutable hooks
- public IReadOnlyList GetHooks() => this._hooks.ToList().AsReadOnly();
+ /// Enumeration of
+ public IEnumerable GetHooks() => this._hooks.Reverse();
///
/// Removes all hooks from the client
@@ -101,7 +160,8 @@ public async Task GetBooleanValue(string flagKey, bool defaultValue, Evalu
/// Resolved flag details
public async Task> GetBooleanDetails(string flagKey, bool defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null) =>
- await this.EvaluateFlag(this._featureProvider.ResolveBooleanValue, FlagValueType.Boolean, flagKey,
+ await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue),
+ FlagValueType.Boolean, flagKey,
defaultValue, context, config);
///
@@ -126,7 +186,8 @@ public async Task GetStringValue(string flagKey, string defaultValue, Ev
/// Resolved flag details
public async Task> GetStringDetails(string flagKey, string defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null) =>
- await this.EvaluateFlag(this._featureProvider.ResolveStringValue, FlagValueType.String, flagKey,
+ await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue),
+ FlagValueType.String, flagKey,
defaultValue, context, config);
///
@@ -151,7 +212,8 @@ public async Task GetIntegerValue(string flagKey, int defaultValue, Evaluat
/// Resolved flag details
public async Task> GetIntegerDetails(string flagKey, int defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null) =>
- await this.EvaluateFlag(this._featureProvider.ResolveIntegerValue, FlagValueType.Number, flagKey,
+ await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue),
+ FlagValueType.Number, flagKey,
defaultValue, context, config);
///
@@ -177,7 +239,8 @@ public async Task GetDoubleValue(string flagKey, double defaultValue,
/// Resolved flag details
public async Task> GetDoubleDetails(string flagKey, double defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null) =>
- await this.EvaluateFlag(this._featureProvider.ResolveDoubleValue, FlagValueType.Number, flagKey,
+ await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue),
+ FlagValueType.Number, flagKey,
defaultValue, context, config);
///
@@ -202,14 +265,18 @@ public async Task GetObjectValue(string flagKey, Value defaultValue, Eval
/// Resolved flag details
public async Task> GetObjectDetails(string flagKey, Value defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null) =>
- await this.EvaluateFlag(this._featureProvider.ResolveStructureValue, FlagValueType.Object, flagKey,
+ await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue),
+ FlagValueType.Object, flagKey,
defaultValue, context, config);
private async Task> EvaluateFlag(
- Func>> resolveValueDelegate,
+ (Func>>, FeatureProvider) providerInfo,
FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext context = null,
FlagEvaluationOptions options = null)
{
+ var resolveValueDelegate = providerInfo.Item1;
+ var provider = providerInfo.Item2;
+
// New up a evaluation context if one was not provided.
if (context == null)
{
@@ -225,9 +292,9 @@ private async Task> EvaluateFlag(
var allHooks = new List()
.Concat(OpenFeature.Instance.GetHooks())
- .Concat(this._hooks)
+ .Concat(this.GetHooks())
.Concat(options?.Hooks ?? Enumerable.Empty())
- .Concat(this._featureProvider.GetProviderHooks())
+ .Concat(provider.GetProviderHooks())
.ToList()
.AsReadOnly();
@@ -241,7 +308,7 @@ private async Task> EvaluateFlag(
flagKey,
defaultValue,
flagValueType, this._metadata,
- OpenFeature.Instance.GetProviderMetadata(),
+ provider.GetMetadata(),
evaluationContextBuilder.Build()
);
diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs
index 6babbdf5..519fe452 100644
--- a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs
+++ b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
using System.Threading.Tasks;
using AutoFixture;
using FluentAssertions;
@@ -30,14 +32,14 @@ public void OpenFeatureClient_Should_Allow_Hooks()
client.AddHooks(new[] { hook1, hook2 });
client.GetHooks().Should().ContainInOrder(hook1, hook2);
- client.GetHooks().Count.Should().Be(2);
+ client.GetHooks().Count().Should().Be(2);
client.AddHooks(hook3);
client.GetHooks().Should().ContainInOrder(hook1, hook2, hook3);
- client.GetHooks().Count.Should().Be(3);
+ client.GetHooks().Count().Should().Be(3);
client.ClearHooks();
- client.GetHooks().Count.Should().Be(0);
+ Assert.Empty(client.GetHooks());
}
[Fact]
@@ -68,7 +70,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation()
var defaultIntegerValue = fixture.Create();
var defaultDoubleValue = fixture.Create();
var defaultStructureValue = fixture.Create();
- var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary());
+ var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty);
OpenFeature.Instance.SetProvider(new NoOpFeatureProvider());
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -114,7 +116,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation()
var defaultIntegerValue = fixture.Create();
var defaultDoubleValue = fixture.Create();
var defaultStructureValue = fixture.Create();
- var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary());
+ var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty);
OpenFeature.Instance.SetProvider(new NoOpFeatureProvider());
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -169,7 +171,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc
mockedFeatureProvider.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
mockedFeatureProvider.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(mockedFeatureProvider.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion, mockedLogger.Object);
@@ -206,7 +208,7 @@ public async Task Should_Resolve_BooleanValue()
featureProviderMock.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
featureProviderMock.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(featureProviderMock.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -232,7 +234,7 @@ public async Task Should_Resolve_StringValue()
featureProviderMock.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
featureProviderMock.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(featureProviderMock.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -258,7 +260,7 @@ public async Task Should_Resolve_IntegerValue()
featureProviderMock.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
featureProviderMock.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(featureProviderMock.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -284,7 +286,7 @@ public async Task Should_Resolve_DoubleValue()
featureProviderMock.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
featureProviderMock.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(featureProviderMock.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -310,7 +312,7 @@ public async Task Should_Resolve_StructureValue()
featureProviderMock.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
featureProviderMock.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(featureProviderMock.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -338,7 +340,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error()
featureProviderMock.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
featureProviderMock.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(featureProviderMock.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -367,7 +369,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error()
featureProviderMock.Setup(x => x.GetMetadata())
.Returns(new Metadata(fixture.Create()));
featureProviderMock.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
OpenFeature.Instance.SetProvider(featureProviderMock.Object);
var client = OpenFeature.Instance.GetClient(clientName, clientVersion);
@@ -380,10 +382,11 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error()
}
[Fact]
- public void Should_Throw_ArgumentNullException_When_Provider_Is_Null()
+ public async Task Should_Use_No_Op_When_Provider_Is_Null()
{
- TestProvider provider = null;
- Assert.Throws(() => new FeatureClient(provider, "test", "test"));
+ OpenFeature.Instance.SetProvider(null);
+ var client = new FeatureClient("test", "test");
+ (await client.GetIntegerValue("some-key", 12)).Should().Be(12);
}
[Fact]
diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs
index 0c97e18b..3b13e6b8 100644
--- a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs
+++ b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
using System.Threading.Tasks;
using AutoFixture;
using FluentAssertions;
@@ -83,7 +85,7 @@ public async Task Hooks_Should_Be_Called_In_Order()
client.AddHooks(clientHook.Object);
await client.GetBooleanValue(flagName, defaultValue, EvaluationContext.Empty,
- new FlagEvaluationOptions(invocationHook.Object, new Dictionary()));
+ new FlagEvaluationOptions(invocationHook.Object, ImmutableDictionary.Empty));
apiHook.Verify(x => x.Before(
It.IsAny>(), It.IsAny>()), Times.Once);
@@ -173,19 +175,19 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook()
FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName),
evaluationContext);
- hook1.Setup(x => x.Before(It.IsAny>(), It.IsAny>()))
+ hook1.Setup(x => x.Before(It.IsAny>(), It.IsAny>()))
.ReturnsAsync(evaluationContext);
hook2.Setup(x =>
- x.Before(hookContext, It.IsAny>()))
+ x.Before(hookContext, It.IsAny>()))
.ReturnsAsync(evaluationContext);
var client = OpenFeature.Instance.GetClient("test", "1.0.0");
await client.GetBooleanValue("test", false, EvaluationContext.Empty,
- new FlagEvaluationOptions(new[] { hook1.Object, hook2.Object }, new Dictionary()));
+ new FlagEvaluationOptions(ImmutableList.Create(hook1.Object, hook2.Object), ImmutableDictionary.Empty));
- hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once);
- hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), It.IsAny>()), Times.Once);
+ hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once);
+ hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), It.IsAny>()), Times.Once);
}
[Fact]
@@ -232,7 +234,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order()
.Returns(new Metadata(null));
provider.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
provider.Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new ResolutionDetails("test", true));
@@ -240,12 +242,12 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order()
OpenFeature.Instance.SetProvider(provider.Object);
var hook = new Mock(MockBehavior.Strict);
- hook.Setup(x => x.Before(It.IsAny>(), It.IsAny>()))
+ hook.Setup(x => x.Before(It.IsAny>(), It.IsAny>()))
.ReturnsAsync(hookContext);
var client = OpenFeature.Instance.GetClient("test", "1.0.0", null, clientContext);
- await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(new[] { hook.Object }, new Dictionary()));
+ await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook.Object), ImmutableDictionary.Empty));
// after proper merging, all properties should equal true
provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y =>
@@ -307,7 +309,7 @@ public async Task Hook_Should_Execute_In_Correct_Order()
.Returns(new Metadata(null));
featureProvider.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
hook.InSequence(sequence).Setup(x =>
x.Before(It.IsAny>(), It.IsAny>()))
@@ -351,10 +353,10 @@ public async Task Register_Hooks_Should_Be_Available_At_All_Levels()
var client = OpenFeature.Instance.GetClient();
client.AddHooks(hook2.Object);
await client.GetBooleanValue("test", false, null,
- new FlagEvaluationOptions(hook3.Object, new Dictionary()));
+ new FlagEvaluationOptions(hook3.Object, ImmutableDictionary.Empty));
- OpenFeature.Instance.GetHooks().Count.Should().Be(1);
- client.GetHooks().Count.Should().Be(1);
+ Assert.Single(OpenFeature.Instance.GetHooks());
+ client.GetHooks().Count().Should().Be(1);
testProvider.GetProviderHooks().Count.Should().Be(1);
}
@@ -372,7 +374,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination()
.Returns(new Metadata(null));
featureProvider.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
hook1.InSequence(sequence).Setup(x =>
x.Before(It.IsAny>(), null))
@@ -405,7 +407,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination()
OpenFeature.Instance.SetProvider(featureProvider.Object);
var client = OpenFeature.Instance.GetClient();
client.AddHooks(new[] { hook1.Object, hook2.Object });
- client.GetHooks().Count.Should().Be(2);
+ client.GetHooks().Count().Should().Be(2);
await client.GetBooleanValue("test", false);
@@ -431,7 +433,7 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination()
featureProvider1.Setup(x => x.GetMetadata())
.Returns(new Metadata(null));
featureProvider1.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
hook1.InSequence(sequence).Setup(x =>
x.Before(It.IsAny>(), null))
@@ -478,7 +480,7 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_
featureProvider.Setup(x => x.GetMetadata())
.Returns(new Metadata(null));
featureProvider.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
hook1.InSequence(sequence).Setup(x =>
x.Before(It.IsAny>(), It.IsAny>()))
@@ -518,7 +520,7 @@ public async Task Hook_Hints_May_Be_Optional()
.Returns(new Metadata(null));
featureProvider.Setup(x => x.GetProviderHooks())
- .Returns(Array.Empty());
+ .Returns(ImmutableList.Empty);
hook.InSequence(sequence)
.Setup(x => x.Before(It.IsAny>(), defaultEmptyHookHints))
diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs
index 5f9a778b..8a96e84b 100644
--- a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs
+++ b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using FluentAssertions;
using Moq;
using OpenFeatureSDK.Constant;
@@ -34,18 +35,18 @@ public void OpenFeature_Should_Add_Hooks()
openFeature.AddHooks(hook1);
openFeature.GetHooks().Should().Contain(hook1);
- openFeature.GetHooks().Count.Should().Be(1);
+ Assert.Single(openFeature.GetHooks());
openFeature.AddHooks(hook2);
openFeature.GetHooks().Should().ContainInOrder(hook1, hook2);
- openFeature.GetHooks().Count.Should().Be(2);
+ openFeature.GetHooks().Count().Should().Be(2);
openFeature.AddHooks(new[] { hook3, hook4 });
openFeature.GetHooks().Should().ContainInOrder(hook1, hook2, hook3, hook4);
- openFeature.GetHooks().Count.Should().Be(4);
+ openFeature.GetHooks().Count().Should().Be(4);
openFeature.ClearHooks();
- openFeature.GetHooks().Count.Should().Be(0);
+ Assert.Empty(openFeature.GetHooks());
}
[Fact]
diff --git a/test/OpenFeatureSDK.Tests/TestImplementations.cs b/test/OpenFeatureSDK.Tests/TestImplementations.cs
index 4236077c..2da88c7b 100644
--- a/test/OpenFeatureSDK.Tests/TestImplementations.cs
+++ b/test/OpenFeatureSDK.Tests/TestImplementations.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Threading.Tasks;
using OpenFeatureSDK.Model;
@@ -39,7 +40,7 @@ public class TestProvider : FeatureProvider
public void AddHook(Hook hook) => this._hooks.Add(hook);
- public override IReadOnlyList GetProviderHooks() => this._hooks.AsReadOnly();
+ public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList();
public override Metadata GetMetadata()
{