diff --git a/README.md b/README.md
index b625137b..1c48aa9a 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ public async Task Example()
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. |
-| ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
+| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
@@ -167,6 +167,43 @@ client.AddHooks(new ExampleClientHook());
var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook()));
```
+### Eventing
+
+Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes,
+provider readiness, or error conditions.
+Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
+Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`.
+
+Please refer to the documentation of the provider you're using to see what events are supported.
+
+Example usage of an Event handler:
+
+```csharp
+public static void EventHandler(ProviderEventPayload eventDetails)
+{
+ Console.WriteLine(eventDetails.Type);
+}
+```
+
+```csharp
+EventHandlerDelegate callback = EventHandler;
+// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event
+Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback);
+```
+
+It is also possible to register an event handler for a specific client, as in the following example:
+
+```csharp
+EventHandlerDelegate callback = EventHandler;
+
+var myClient = Api.Instance.GetClient("my-client");
+
+var provider = new ExampleProvider();
+await Api.Instance.SetProvider(myClient.GetMetadata().Name, provider);
+
+myClient.AddHandler(ProviderEventTypes.ProviderReady, callback);
+```
+
### Logging
The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation.
diff --git a/build/Common.props b/build/Common.props
index 4af79a3c..42a91d64 100644
--- a/build/Common.props
+++ b/build/Common.props
@@ -25,5 +25,6 @@
+
diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs
index 3583572e..8d0e22f5 100644
--- a/src/OpenFeature/Api.cs
+++ b/src/OpenFeature/Api.cs
@@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using OpenFeature.Constant;
using OpenFeature.Model;
namespace OpenFeature
@@ -13,7 +14,7 @@ namespace OpenFeature
/// In the absence of a provider the evaluation API uses the "No-op provider", which simply returns the supplied default flag value.
///
///
- public sealed class Api
+ public sealed class Api : IEventBus
{
private EvaluationContext _evaluationContext = EvaluationContext.Empty;
private readonly ProviderRepository _repository = new ProviderRepository();
@@ -22,6 +23,8 @@ public sealed class Api
/// The reader/writer locks are not disposed because the singleton instance should never be disposed.
private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim();
+ internal readonly EventExecutor EventExecutor = new EventExecutor();
+
///
/// Singleton instance of Api
@@ -42,6 +45,7 @@ private Api() { }
/// Implementation of
public async Task SetProvider(FeatureProvider featureProvider)
{
+ this.EventExecutor.RegisterDefaultFeatureProvider(featureProvider);
await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false);
}
@@ -54,6 +58,7 @@ public async Task SetProvider(FeatureProvider featureProvider)
/// Implementation of
public async Task SetProvider(string clientName, FeatureProvider featureProvider)
{
+ this.EventExecutor.RegisterClientFeatureProvider(clientName, featureProvider);
await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false);
}
@@ -201,6 +206,28 @@ public EvaluationContext GetContext()
public async Task Shutdown()
{
await this._repository.Shutdown().ConfigureAwait(false);
+ await this.EventExecutor.Shutdown().ConfigureAwait(false);
+ }
+
+ ///
+ public void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ this.EventExecutor.AddApiLevelHandler(type, handler);
+ }
+
+ ///
+ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ this.EventExecutor.RemoveApiLevelHandler(type, handler);
+ }
+
+ ///
+ /// Sets the logger for the API
+ ///
+ /// The logger to be used
+ public void SetLogger(ILogger logger)
+ {
+ this.EventExecutor.Logger = logger;
}
}
}
diff --git a/src/OpenFeature/Constant/EventType.cs b/src/OpenFeature/Constant/EventType.cs
new file mode 100644
index 00000000..3d3c9dc8
--- /dev/null
+++ b/src/OpenFeature/Constant/EventType.cs
@@ -0,0 +1,25 @@
+namespace OpenFeature.Constant
+{
+ ///
+ /// The ProviderEventTypes enum represents the available event types of a provider.
+ ///
+ public enum ProviderEventTypes
+ {
+ ///
+ /// ProviderReady should be emitted by a provider upon completing its initialisation.
+ ///
+ ProviderReady,
+ ///
+ /// ProviderError should be emitted by a provider upon encountering an error.
+ ///
+ ProviderError,
+ ///
+ /// ProviderConfigurationChanged should be emitted by a provider when a flag configuration has been changed.
+ ///
+ ProviderConfigurationChanged,
+ ///
+ /// ProviderStale should be emitted by a provider when it goes into the stale state.
+ ///
+ ProviderStale
+ }
+}
diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs
new file mode 100644
index 00000000..8a6df9a4
--- /dev/null
+++ b/src/OpenFeature/EventExecutor.cs
@@ -0,0 +1,371 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+namespace OpenFeature
+{
+
+ internal delegate Task ShutdownDelegate();
+
+ internal class EventExecutor
+ {
+ private readonly object _lockObj = new object();
+ public readonly Channel EventChannel = Channel.CreateBounded(1);
+ private FeatureProviderReference _defaultProvider;
+ private readonly Dictionary _namedProviderReferences = new Dictionary();
+ private readonly List _activeSubscriptions = new List();
+ private readonly SemaphoreSlim _shutdownSemaphore = new SemaphoreSlim(0);
+
+ private ShutdownDelegate _shutdownDelegate;
+
+ private readonly Dictionary> _apiHandlers = new Dictionary>();
+ private readonly Dictionary>> _clientHandlers = new Dictionary>>();
+
+ internal ILogger Logger { get; set; }
+
+ public EventExecutor()
+ {
+ this.Logger = new Logger(new NullLoggerFactory());
+ this._shutdownDelegate = this.SignalShutdownAsync;
+ var eventProcessing = new Thread(this.ProcessEventAsync);
+ eventProcessing.Start();
+ }
+
+ internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
+ {
+ if (!this._apiHandlers.TryGetValue(eventType, out var eventHandlers))
+ {
+ eventHandlers = new List();
+ this._apiHandlers[eventType] = eventHandlers;
+ }
+
+ eventHandlers.Add(handler);
+
+ this.EmitOnRegistration(this._defaultProvider, eventType, handler);
+ }
+ }
+
+ internal void RemoveApiLevelHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
+ {
+ if (this._apiHandlers.TryGetValue(type, out var eventHandlers))
+ {
+ eventHandlers.Remove(handler);
+ }
+ }
+ }
+
+ internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
+ {
+ // check if there is already a list of handlers for the given client and event type
+ if (!this._clientHandlers.TryGetValue(client, out var registry))
+ {
+ registry = new Dictionary>();
+ this._clientHandlers[client] = registry;
+ }
+
+ if (!this._clientHandlers[client].TryGetValue(eventType, out var eventHandlers))
+ {
+ eventHandlers = new List();
+ this._clientHandlers[client][eventType] = eventHandlers;
+ }
+
+ this._clientHandlers[client][eventType].Add(handler);
+
+ this.EmitOnRegistration(
+ this._namedProviderReferences.TryGetValue(client, out var clientProviderReference)
+ ? clientProviderReference
+ : this._defaultProvider, eventType, handler);
+ }
+ }
+
+ internal void RemoveClientHandler(string client, ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
+ {
+ if (this._clientHandlers.TryGetValue(client, out var clientEventHandlers))
+ {
+ if (clientEventHandlers.TryGetValue(type, out var eventHandlers))
+ {
+ eventHandlers.Remove(handler);
+ }
+ }
+ }
+ }
+
+ internal void RegisterDefaultFeatureProvider(FeatureProvider provider)
+ {
+ if (provider == null)
+ {
+ return;
+ }
+ lock (this._lockObj)
+ {
+ var oldProvider = this._defaultProvider;
+
+ this._defaultProvider = new FeatureProviderReference(provider);
+
+ this.StartListeningAndShutdownOld(this._defaultProvider, oldProvider);
+ }
+ }
+
+ internal void RegisterClientFeatureProvider(string client, FeatureProvider provider)
+ {
+ if (provider == null)
+ {
+ return;
+ }
+ lock (this._lockObj)
+ {
+ var newProvider = new FeatureProviderReference(provider);
+ FeatureProviderReference oldProvider = null;
+ if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider))
+ {
+ oldProvider = foundOldProvider;
+ }
+
+ this._namedProviderReferences[client] = newProvider;
+
+ this.StartListeningAndShutdownOld(newProvider, oldProvider);
+ }
+ }
+
+ private void StartListeningAndShutdownOld(FeatureProviderReference newProvider, FeatureProviderReference oldProvider)
+ {
+ // check if the provider is already active - if not, we need to start listening for its emitted events
+ if (!this.IsProviderActive(newProvider))
+ {
+ this._activeSubscriptions.Add(newProvider);
+ var featureProviderEventProcessing = new Thread(this.ProcessFeatureProviderEventsAsync);
+ featureProviderEventProcessing.Start(newProvider);
+ }
+
+ if (oldProvider != null && !this.IsProviderBound(oldProvider))
+ {
+ this._activeSubscriptions.Remove(oldProvider);
+ var channel = oldProvider.Provider.GetEventChannel();
+ if (channel != null)
+ {
+ channel.Writer.WriteAsync(new ShutdownSignal());
+ }
+ }
+ }
+
+ private bool IsProviderBound(FeatureProviderReference provider)
+ {
+ if (this._defaultProvider == provider)
+ {
+ return true;
+ }
+ foreach (var providerReference in this._namedProviderReferences.Values)
+ {
+ if (providerReference == provider)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool IsProviderActive(FeatureProviderReference providerRef)
+ {
+ return this._activeSubscriptions.Contains(providerRef);
+ }
+
+ private void EmitOnRegistration(FeatureProviderReference provider, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ {
+ if (provider == null)
+ {
+ return;
+ }
+ var status = provider.Provider.GetStatus();
+
+ var message = "";
+ if (status == ProviderStatus.Ready && eventType == ProviderEventTypes.ProviderReady)
+ {
+ message = "Provider is ready";
+ }
+ else if (status == ProviderStatus.Error && eventType == ProviderEventTypes.ProviderError)
+ {
+ message = "Provider is in error state";
+ }
+ else if (status == ProviderStatus.Stale && eventType == ProviderEventTypes.ProviderStale)
+ {
+ message = "Provider is in stale state";
+ }
+
+ if (message != "")
+ {
+ try
+ {
+ handler.Invoke(new ProviderEventPayload
+ {
+ ProviderName = provider.Provider?.GetMetadata()?.Name,
+ Type = eventType,
+ Message = message
+ });
+ }
+ catch (Exception exc)
+ {
+ this.Logger?.LogError("Error running handler: " + exc);
+ }
+ }
+ }
+
+ private async void ProcessFeatureProviderEventsAsync(object providerRef)
+ {
+ while (true)
+ {
+ var typedProviderRef = (FeatureProviderReference)providerRef;
+ if (typedProviderRef.Provider.GetEventChannel() == null)
+ {
+ return;
+ }
+ var item = await typedProviderRef.Provider.GetEventChannel().Reader.ReadAsync().ConfigureAwait(false);
+
+ switch (item)
+ {
+ case ProviderEventPayload eventPayload:
+ await this.EventChannel.Writer.WriteAsync(new Event { Provider = typedProviderRef, EventPayload = eventPayload }).ConfigureAwait(false);
+ break;
+ case ShutdownSignal _:
+ typedProviderRef.ShutdownSemaphore.Release();
+ return;
+ }
+ }
+ }
+
+ // Method to process events
+ private async void ProcessEventAsync()
+ {
+ while (true)
+ {
+ var item = await this.EventChannel.Reader.ReadAsync().ConfigureAwait(false);
+
+ switch (item)
+ {
+ case Event e:
+ lock (this._lockObj)
+ {
+ if (this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers))
+ {
+ foreach (var eventHandler in eventHandlers)
+ {
+ this.InvokeEventHandler(eventHandler, e);
+ }
+ }
+
+ // look for client handlers and call invoke method there
+ foreach (var keyAndValue in this._namedProviderReferences)
+ {
+ if (keyAndValue.Value == e.Provider)
+ {
+ if (this._clientHandlers.TryGetValue(keyAndValue.Key, out var clientRegistry))
+ {
+ if (clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers))
+ {
+ foreach (var eventHandler in clientEventHandlers)
+ {
+ this.InvokeEventHandler(eventHandler, e);
+ }
+ }
+ }
+ }
+ }
+
+ if (e.Provider != this._defaultProvider)
+ {
+ break;
+ }
+ // handling the default provider - invoke event handlers for clients which are not bound
+ // to a particular feature provider
+ foreach (var keyAndValues in this._clientHandlers)
+ {
+ if (this._namedProviderReferences.TryGetValue(keyAndValues.Key, out _))
+ {
+ // if there is an association for the client to a specific feature provider, then continue
+ continue;
+ }
+ if (keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers))
+ {
+ foreach (var eventHandler in clientEventHandlers)
+ {
+ this.InvokeEventHandler(eventHandler, e);
+ }
+ }
+ }
+ }
+ break;
+ case ShutdownSignal _:
+ this._shutdownSemaphore.Release();
+ return;
+ }
+
+ }
+ }
+
+ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e)
+ {
+ try
+ {
+ eventHandler.Invoke(e.EventPayload);
+ }
+ catch (Exception exc)
+ {
+ this.Logger?.LogError("Error running handler: " + exc);
+ }
+ }
+
+ public async Task Shutdown()
+ {
+ await this._shutdownDelegate().ConfigureAwait(false);
+ }
+
+ internal void SetShutdownDelegate(ShutdownDelegate del)
+ {
+ this._shutdownDelegate = del;
+ }
+
+ // Method to signal shutdown
+ private async Task SignalShutdownAsync()
+ {
+ // Enqueue a shutdown signal
+ await this.EventChannel.Writer.WriteAsync(new ShutdownSignal()).ConfigureAwait(false);
+
+ // Wait for the processing loop to acknowledge the shutdown
+ await this._shutdownSemaphore.WaitAsync().ConfigureAwait(false);
+ }
+ }
+
+ internal class ShutdownSignal
+ {
+ }
+
+ internal class FeatureProviderReference
+ {
+ internal readonly SemaphoreSlim ShutdownSemaphore = new SemaphoreSlim(0);
+ internal FeatureProvider Provider { get; }
+
+ public FeatureProviderReference(FeatureProvider provider)
+ {
+ this.Provider = provider;
+ }
+ }
+
+ internal class Event
+ {
+ internal FeatureProviderReference Provider { get; set; }
+ internal ProviderEventPayload EventPayload { get; set; }
+ }
+}
diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs
index c3cc1406..3dd85102 100644
--- a/src/OpenFeature/FeatureProvider.cs
+++ b/src/OpenFeature/FeatureProvider.cs
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
+using System.Threading.Channels;
using System.Threading.Tasks;
using OpenFeature.Constant;
using OpenFeature.Model;
@@ -25,6 +26,11 @@ public abstract class FeatureProvider
/// Immutable list of hooks
public virtual IImmutableList GetProviderHooks() => ImmutableList.Empty;
+ ///
+ /// The event channel of the provider.
+ ///
+ protected Channel EventChannel = Channel.CreateBounded(1);
+
///
/// Metadata describing the provider.
///
@@ -105,7 +111,7 @@ public abstract Task> ResolveStructureValue(string flag
///
///
/// A provider which supports initialization should override this method as well as
- /// .
+ /// .
///
///
/// The provider should return or from
@@ -128,5 +134,11 @@ public virtual Task Shutdown()
// Intentionally left blank.
return Task.CompletedTask;
}
+
+ ///
+ /// Returns the event channel of the provider.
+ ///
+ /// The event channel of the provider
+ public virtual Channel GetEventChannel() => this.EventChannel;
}
}
diff --git a/src/OpenFeature/IEventBus.cs b/src/OpenFeature/IEventBus.cs
new file mode 100644
index 00000000..114b66b3
--- /dev/null
+++ b/src/OpenFeature/IEventBus.cs
@@ -0,0 +1,24 @@
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+namespace OpenFeature
+{
+ ///
+ /// Defines the methods required for handling events.
+ ///
+ public interface IEventBus
+ {
+ ///
+ /// Adds an Event Handler for the given event type.
+ ///
+ /// The type of the event
+ /// Implementation of the
+ void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler);
+ ///
+ /// Removes an Event Handler for the given event type.
+ ///
+ /// The type of the event
+ /// Implementation of the
+ void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler);
+ }
+}
diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs
index 2186ca68..1d2e6dfb 100644
--- a/src/OpenFeature/IFeatureClient.cs
+++ b/src/OpenFeature/IFeatureClient.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
+using OpenFeature.Constant;
using OpenFeature.Model;
namespace OpenFeature
@@ -7,7 +8,7 @@ namespace OpenFeature
///
/// Interface used to resolve flags of varying types.
///
- public interface IFeatureClient
+ public interface IFeatureClient : IEventBus
{
///
/// Appends hooks to client
diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs
new file mode 100644
index 00000000..da68aef4
--- /dev/null
+++ b/src/OpenFeature/Model/ProviderEvents.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using OpenFeature.Constant;
+
+namespace OpenFeature.Model
+{
+ ///
+ /// The EventHandlerDelegate is an implementation of an Event Handler
+ ///
+ public delegate void EventHandlerDelegate(ProviderEventPayload eventDetails);
+
+ ///
+ /// Contains the payload of an OpenFeature Event.
+ ///
+ public class ProviderEventPayload
+ {
+ ///
+ /// Name of the provider.
+ ///
+ public string ProviderName { get; set; }
+
+ ///
+ /// Type of the event
+ ///
+ public ProviderEventTypes Type { get; set; }
+
+ ///
+ /// A message providing more information about the event.
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// A List of flags that have been changed.
+ ///
+ public List FlagsChanged { get; set; }
+
+ ///
+ /// Metadata information for the event.
+ ///
+ public Dictionary EventMetadata { get; set; }
+ }
+}
diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs
index 1cc26802..9ea9b13a 100644
--- a/src/OpenFeature/OpenFeatureClient.cs
+++ b/src/OpenFeature/OpenFeatureClient.cs
@@ -93,6 +93,18 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat
/// Hook that implements the interface
public void AddHooks(Hook hook) => this._hooks.Push(hook);
+ ///
+ public void AddHandler(ProviderEventTypes eventType, EventHandlerDelegate handler)
+ {
+ Api.Instance.EventExecutor.AddClientHandler(this._metadata.Name, eventType, handler);
+ }
+
+ ///
+ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ Api.Instance.EventExecutor.RemoveClientHandler(this._metadata.Name, type, handler);
+ }
+
///
public void AddHooks(IEnumerable hooks) => this._hooks.PushRange(hooks.ToArray());
diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs
index aa79cbf9..8d679f94 100644
--- a/test/OpenFeature.Tests/FeatureProviderTests.cs
+++ b/test/OpenFeature.Tests/FeatureProviderTests.cs
@@ -19,7 +19,7 @@ public void Provider_Must_Have_Metadata()
{
var provider = new TestProvider();
- provider.GetMetadata().Name.Should().Be(TestProvider.Name);
+ provider.GetMetadata().Name.Should().Be(TestProvider.DefaultName);
}
[Fact]
diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs
new file mode 100644
index 00000000..8c183e63
--- /dev/null
+++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs
@@ -0,0 +1,445 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using AutoFixture;
+using NSubstitute;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+using OpenFeature.Tests.Internal;
+using Xunit;
+
+namespace OpenFeature.Tests
+{
+ [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")]
+ public class OpenFeatureEventTest : ClearOpenFeatureInstanceFixture
+ {
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler()
+ {
+ var eventHandler = Substitute.For();
+
+ var eventExecutor = new EventExecutor();
+
+ eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler);
+
+ var eventMetadata = new Dictionary { { "foo", "bar" } };
+ var myEvent = new Event
+ {
+ EventPayload = new ProviderEventPayload
+ {
+ Type = ProviderEventTypes.ProviderConfigurationChanged,
+ Message = "The provider is ready",
+ EventMetadata = eventMetadata,
+ FlagsChanged = new List { "flag1", "flag2" }
+ }
+ };
+ eventExecutor.EventChannel.Writer.TryWrite(myEvent);
+
+ Thread.Sleep(1000);
+
+ eventHandler.Received().Invoke(Arg.Is(payload => payload == myEvent.EventPayload));
+
+ // shut down the event executor
+ await eventExecutor.Shutdown();
+
+ // the next event should not be propagated to the event handler
+ var newEventPayload = new ProviderEventPayload { Type = ProviderEventTypes.ProviderStale };
+
+ eventExecutor.EventChannel.Writer.TryWrite(newEventPayload);
+
+ eventHandler.DidNotReceive().Invoke(newEventPayload);
+
+ eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.Type == ProviderEventTypes.ProviderStale));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ public async Task API_Level_Event_Handlers_Should_Be_Registered()
+ {
+ var eventHandler = Substitute.For();
+
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler);
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler);
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler);
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(testProvider);
+
+ testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged);
+ testProvider.SendEvent(ProviderEventTypes.ProviderError);
+ testProvider.SendEvent(ProviderEventTypes.ProviderStale);
+
+ Thread.Sleep(1000);
+ eventHandler
+ .Received()
+ .Invoke(
+ Arg.Is(
+ payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady
+ ));
+
+ eventHandler
+ .Received()
+ .Invoke(
+ Arg.Is(
+ payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged
+ ));
+
+ eventHandler
+ .Received()
+ .Invoke(
+ Arg.Is(
+ payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError
+ ));
+
+ eventHandler
+ .Received()
+ .Invoke(
+ Arg.Is(
+ payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale
+ ));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")]
+ [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")]
+ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Ready()
+ {
+ var eventHandler = Substitute.For();
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(testProvider);
+
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ Thread.Sleep(1000);
+ eventHandler
+ .Received()
+ .Invoke(
+ Arg.Is(
+ payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady
+ ));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.3.2", "If the provider's `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.")]
+ [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")]
+ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_After_Registering_Provider_Error()
+ {
+ var eventHandler = Substitute.For();
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(testProvider);
+
+ testProvider.SetStatus(ProviderStatus.Error);
+
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler);
+
+ Thread.Sleep(1000);
+ eventHandler
+ .Received()
+ .Invoke(
+ Arg.Is(
+ payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError
+ ));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")]
+ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_After_Registering_Provider_Stale()
+ {
+ var eventHandler = Substitute.For();
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(testProvider);
+
+ testProvider.SetStatus(ProviderStatus.Stale);
+
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler);
+
+ Thread.Sleep(1000);
+ eventHandler
+ .Received()
+ .Invoke(
+ Arg.Is(
+ payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale
+ ));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")]
+ public async Task API_Level_Event_Handlers_Should_Be_Exchangeable()
+ {
+ var eventHandler = Substitute.For();
+
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler);
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(testProvider);
+
+ testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged);
+
+ var newTestProvider = new TestProvider();
+ await Api.Instance.SetProvider(newTestProvider);
+
+ newTestProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged);
+
+ Thread.Sleep(1000);
+ eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady));
+ eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged));
+ }
+
+ [Fact]
+ [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")]
+ public async Task API_Level_Event_Handlers_Should_Be_Removable()
+ {
+ var eventHandler = Substitute.For();
+
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(testProvider);
+
+ Thread.Sleep(1000);
+ Api.Instance.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ var newTestProvider = new TestProvider();
+ await Api.Instance.SetProvider(newTestProvider);
+
+ eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")]
+ public async Task API_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails()
+ {
+ var fixture = new Fixture();
+
+ var failingEventHandler = Substitute.For();
+ var eventHandler = Substitute.For();
+
+ failingEventHandler.When(x => x.Invoke(Arg.Any()))
+ .Do(x => throw new Exception());
+
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler);
+ Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ var testProvider = new TestProvider(fixture.Create());
+ await Api.Instance.SetProvider(testProvider);
+
+ failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ public async Task Client_Level_Event_Handlers_Should_Be_Registered()
+ {
+ var fixture = new Fixture();
+ var eventHandler = Substitute.For();
+
+ var myClient = Api.Instance.GetClient(fixture.Create());
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider);
+
+ myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")]
+ public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails()
+ {
+ var fixture = new Fixture();
+
+ var failingEventHandler = Substitute.For();
+ var eventHandler = Substitute.For();
+
+ failingEventHandler.When(x => x.Invoke(Arg.Any()))
+ .Do(x => throw new Exception());
+
+ var myClient = Api.Instance.GetClient(fixture.Create());
+
+ myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler);
+ myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider);
+
+ Thread.Sleep(1000);
+
+ failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")]
+ [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Provider()
+ {
+ var fixture = new Fixture();
+ var eventHandler = Substitute.For();
+ var clientEventHandler = Substitute.For();
+
+ var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create());
+ var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create());
+
+ var apiProvider = new TestProvider(fixture.Create());
+ var clientProvider = new TestProvider(fixture.Create());
+
+ // set the default provider on API level, but not specifically to the client
+ await Api.Instance.SetProvider(apiProvider);
+ // set the other provider specifically for the client
+ await Api.Instance.SetProvider(myClientWithBoundProvider.GetMetadata().Name, clientProvider);
+
+ myClientWithNoBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+ myClientWithBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, clientEventHandler);
+
+ eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name));
+ eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name));
+
+ clientEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name));
+ clientEventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")]
+ [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")]
+ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Named_Provider_Instead_of_Default()
+ {
+ var fixture = new Fixture();
+ var clientEventHandler = Substitute.For();
+
+ var client = Api.Instance.GetClient(fixture.Create());
+
+ var defaultProvider = new TestProvider(fixture.Create());
+ var clientProvider = new TestProvider(fixture.Create());
+
+ // set the default provider
+ await Api.Instance.SetProvider(defaultProvider);
+
+ client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler);
+
+ defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged);
+
+ Thread.Sleep(1000);
+
+ // verify that the client received the event from the default provider as there is no named provider registered yet
+ clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged));
+
+ // set the other provider specifically for the client
+ await Api.Instance.SetProvider(client.GetMetadata().Name, clientProvider);
+
+ // now, send another event for the default handler
+ defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged);
+ clientProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged);
+
+ Thread.Sleep(1000);
+
+ // now the client should have received only the event from the named provider
+ clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged));
+ // for the default provider, the number of received events should stay unchanged
+ clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged));
+ }
+
+ [Fact]
+ [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")]
+ [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")]
+ [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")]
+ public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering()
+ {
+ var fixture = new Fixture();
+ var eventHandler = Substitute.For();
+
+ var myClient = Api.Instance.GetClient(fixture.Create());
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider);
+
+ // add the event handler after the provider has already transitioned into the ready state
+ myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ }
+
+ [Fact]
+ [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")]
+ [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")]
+ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")]
+ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")]
+ [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")]
+ public async Task Client_Level_Event_Handlers_Should_Be_Removable()
+ {
+ var fixture = new Fixture();
+
+ var eventHandler = Substitute.For();
+
+ var myClient = Api.Instance.GetClient(fixture.Create());
+
+ myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ var testProvider = new TestProvider();
+ await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider);
+
+ // wait for the first event to be received
+ Thread.Sleep(1000);
+ myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler);
+
+ // send another event from the provider - this one should not be received
+ testProvider.SendEvent(ProviderEventTypes.ProviderReady);
+
+ // wait a bit and make sure we only have received the first event, but nothing after removing the event handler
+ Thread.Sleep(1000);
+ eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name));
+ }
+ }
+}
diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs
index afcfcd18..3e8b4d3d 100644
--- a/test/OpenFeature.Tests/OpenFeatureTests.cs
+++ b/test/OpenFeature.Tests/OpenFeatureTests.cs
@@ -11,6 +11,11 @@ namespace OpenFeature.Tests
{
public class OpenFeatureTests : ClearOpenFeatureInstanceFixture
{
+ static async Task EmptyShutdown()
+ {
+ await Task.FromResult(0).ConfigureAwait(false);
+ }
+
[Fact]
[Specification("1.1.1", "The `API`, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.")]
public void OpenFeature_Should_Be_Singleton()
@@ -74,6 +79,9 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider()
[Specification("1.6.1", "The API MUST define a mechanism to propagate a shutdown request to active providers.")]
public async Task OpenFeature_Should_Support_Shutdown()
{
+ // configure the shutdown method of the event executor to do nothing
+ // to prevent eventing tests from failing
+ Api.Instance.EventExecutor.SetShutdownDelegate(EmptyShutdown);
var providerA = Substitute.For();
providerA.GetStatus().Returns(ProviderStatus.NotReady);
@@ -96,13 +104,13 @@ public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_P
var openFeature = Api.Instance;
openFeature.SetProvider(new NoOpFeatureProvider());
- openFeature.SetProvider(TestProvider.Name, new TestProvider());
+ openFeature.SetProvider(TestProvider.DefaultName, new TestProvider());
var defaultClient = openFeature.GetProviderMetadata();
- var namedClient = openFeature.GetProviderMetadata(TestProvider.Name);
+ var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName);
defaultClient.Name.Should().Be(NoOpProvider.NoOpProviderName);
- namedClient.Name.Should().Be(TestProvider.Name);
+ namedClient.Name.Should().Be(TestProvider.DefaultName);
}
[Fact]
@@ -115,7 +123,7 @@ public void OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided()
var defaultClient = openFeature.GetProviderMetadata();
- defaultClient.Name.Should().Be(TestProvider.Name);
+ defaultClient.Name.Should().Be(TestProvider.DefaultName);
}
[Fact]
diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs
index e2bcf5e9..9683e7ef 100644
--- a/test/OpenFeature.Tests/TestImplementations.cs
+++ b/test/OpenFeature.Tests/TestImplementations.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
+using OpenFeature.Constant;
using OpenFeature.Model;
namespace OpenFeature.Tests
@@ -36,15 +37,31 @@ public class TestProvider : FeatureProvider
{
private readonly List _hooks = new List();
- public static string Name => "test-provider";
+ public static string DefaultName = "test-provider";
+
+ public string Name { get; set; }
+
+ private ProviderStatus _status;
public void AddHook(Hook hook) => this._hooks.Add(hook);
public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList();
+ public TestProvider()
+ {
+ this._status = ProviderStatus.NotReady;
+ this.Name = DefaultName;
+ }
+
+ public TestProvider(string name)
+ {
+ this._status = ProviderStatus.NotReady;
+ this.Name = name;
+ }
+
public override Metadata GetMetadata()
{
- return new Metadata(Name);
+ return new Metadata(this.Name);
}
public override Task> ResolveBooleanValue(string flagKey, bool defaultValue,
@@ -76,5 +93,27 @@ public override Task> ResolveStructureValue(string flag
{
return Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
}
+
+ public override ProviderStatus GetStatus()
+ {
+ return this._status;
+ }
+
+ public void SetStatus(ProviderStatus status)
+ {
+ this._status = status;
+ }
+
+ public override Task Initialize(EvaluationContext context)
+ {
+ this._status = ProviderStatus.Ready;
+ this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name });
+ return base.Initialize(context);
+ }
+
+ internal void SendEvent(ProviderEventTypes eventType)
+ {
+ this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name });
+ }
}
}