From ac3336d085cb75beced3ec9249ef63e90c6ea06c Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 16 Jan 2024 19:16:59 -0500 Subject: [PATCH 1/9] refactor: Cleanup + plumb cancellation tokens Signed-off-by: Austin Drenski --- README.md | 345 ++++++++++++++++++ src/OpenFeature/Api.cs | 38 +- src/OpenFeature/EventExecutor.cs | 40 +- src/OpenFeature/FeatureProvider.cs | 38 +- src/OpenFeature/Hook.cs | 27 +- src/OpenFeature/IFeatureClient.cs | 31 +- src/OpenFeature/NoOpProvider.cs | 11 +- src/OpenFeature/OpenFeatureClient.cs | 118 +++--- src/OpenFeature/ProviderRepository.cs | 47 ++- .../OpenFeatureClientBenchmarks.cs | 30 +- .../Steps/EvaluationStepDefinitions.cs | 28 +- .../OpenFeature.Tests/FeatureProviderTests.cs | 38 +- .../OpenFeatureClientTests.cs | 110 +++--- .../OpenFeatureEventTests.cs | 20 +- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 292 +++++++-------- test/OpenFeature.Tests/OpenFeatureTests.cs | 76 ++-- .../ProviderRepositoryTests.cs | 191 +++++----- test/OpenFeature.Tests/TestImplementations.cs | 49 +-- 18 files changed, 970 insertions(+), 559 deletions(-) diff --git a/README.md b/README.md index 6cb3c35c..27c2f668 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD @@ -318,3 +319,347 @@ Interested in contributing? Great, we'd love your help! To get started, take a l Made with [contrib.rocks](https://contrib.rocks). +======= + + +

+ + + OpenFeature Logo + +

+ +

OpenFeature .NET SDK

+ + + +

+ + Specification + + + + + Release + + +
+ + Slack + + + Codecov + + + NuGet + + + CII Best Practices + + +

+ + +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. + + + +## 🚀 Quick start + +### Requirements + +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ + +Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 + +### Install + +Use the following to initialize your project: + +```sh +dotnet new console +``` + +and install OpenFeature: + +```sh +dotnet add package OpenFeature +``` + +### Usage + +```csharp +public async Task Example() +{ + // Register your feature flag provider + await Api.Instance.SetProviderAsync(new InMemoryProvider()); + + // Create a new client + FeatureClient client = Api.Instance.GetClient(); + + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false); + + if ( v2Enabled ) + { + //Do some work + } +} +``` + +## 🌟 Features + +| Status | Features | Description | +| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| ✅ | [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. | +| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | + +Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ + +### Providers + +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). + +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. + +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +```csharp +await Api.Instance.SetProviderAsync(new MyProvider()); +``` + +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [named clients](#named-clients), which is covered in more detail below. + +### Targeting + +Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). + +```csharp +// set a value to the global context +EvaluationContextBuilder builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext apiCtx = builder.Build(); +Api.Instance.SetContext(apiCtx); + +// set a value to the client context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext clientCtx = builder.Build(); +var client = Api.Instance.GetClient(); +client.SetContext(clientCtx); + +// set a value to the invocation context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext reqCtx = builder.Build(); + +bool flagValue = await client.GetBooleanValueAsync("some-flag", false, reqCtx); + +``` + +### Hooks + +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/). +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. + +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. + +```csharp +// add a hook globally, to run on all evaluations +Api.Instance.AddHooks(new ExampleGlobalHook()); + +// add a hook on this client, to run on all evaluations made by this client +var client = Api.Instance.GetClient(); +client.AddHooks(new ExampleClientHook()); + +// add a hook for this evaluation only +var value = await client.GetBooleanValueAsync("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); +``` + +### 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. + +### Named clients + +Clients can be given a name. +A name is a logical identifier that can be used to associate clients with a particular provider. +If a name has no associated provider, the global provider is used. + +```csharp +// registering the default provider +await Api.Instance.SetProvider(new LocalProvider()); + +// registering a named provider +await Api.Instance.SetProvider("clientForCache", new CachedProvider()); + +// a client backed by default provider +FeatureClient clientDefault = Api.Instance.GetClient(); + +// a client backed by CachedProvider +FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); + +``` + +### 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.SetProviderAsync(myClient.GetMetadata().Name, provider); + +myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +### Shutdown + +The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. + +```csharp +// Shut down all providers +await Api.Instance.ShutdownAsync(); +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +```csharp +public class MyProvider : FeatureProvider +{ + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + // resolve a boolean flag value + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + // resolve a double flag value + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + // resolve an int flag value + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + // resolve a string flag value + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + // resolve an object flag value + } +} +``` + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`BeforeAsync`/`AfterAsync`/`FinallyAsync`/`ErrorAsync`) need to be defined. + +```csharp +public class MyHook : Hook +{ + public ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + { + // code to run before flag evaluation + } + + public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + { + // code to run after successful flag evaluation + } + + public virtual ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + { + // code to run if there's an error during before hooks or during flag evaluation + } + + public virtual ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + { + // code to run after all other stages, regardless of success/failure + } +} +``` + +Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! + + +## ⭐️ Support the project + +- Give this repo a ⭐️! +- Follow us on social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more information, check out our [community page](https://openfeature.dev/community/) + +## 🤝 Contributing + +Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. + +### Thanks to everyone who has already contributed + + + + + +Made with [contrib.rocks](https://contrib.rocks). + +>>>>>>> f23274b (refactor: Cleanup + plumb cancellation tokens) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index fbafa695..564d838f 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -54,10 +54,11 @@ public void SetProvider(FeatureProvider featureProvider) /// /// The provider cannot be set to null. Attempting to set the provider to null has no effect. /// Implementation of - public async Task SetProviderAsync(FeatureProvider? featureProvider) + /// The . + public async ValueTask SetProviderAsync(FeatureProvider featureProvider, CancellationToken cancellationToken = default) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext(), cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -78,14 +79,11 @@ public void SetProvider(string clientName, FeatureProvider featureProvider) /// /// Name of client /// Implementation of - public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider) + /// The . + public async ValueTask SetProviderAsync(string clientName, FeatureProvider featureProvider, CancellationToken cancellationToken = default) { - if (string.IsNullOrWhiteSpace(clientName)) - { - throw new ArgumentNullException(nameof(clientName)); - } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -248,18 +246,22 @@ public EvaluationContext GetContext() /// Once shut down is complete, API is reset and ready to use again. /// /// - public async Task Shutdown() + /// The . + public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) { - await using (this._eventExecutor.ConfigureAwait(false)) - await using (this._repository.ConfigureAwait(false)) - { - this._evaluationContext = EvaluationContext.Empty; - this._hooks.Clear(); + // TODO: conflict + // await using (this._eventExecutor.ConfigureAwait(false)) + // await using (this._repository.ConfigureAwait(false)) + // { + // this._evaluationContext = EvaluationContext.Empty; + // this._hooks.Clear(); - // TODO: make these lazy to avoid extra allocations on the common cleanup path? - this._eventExecutor = new EventExecutor(); - this._repository = new ProviderRepository(); - } + // // TODO: make these lazy to avoid extra allocations on the common cleanup path? + // this._eventExecutor = new EventExecutor(); + // this._repository = new ProviderRepository(); + // } + // await this._repository.ShutdownAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + // await this.EventExecutor.ShutdownAsync(cancellationToken).ConfigureAwait(false); } /// diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 816bf13e..1fd6d96f 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -10,6 +10,8 @@ namespace OpenFeature { + internal delegate ValueTask ShutdownDelegate(CancellationToken cancellationToken); + internal sealed partial class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); @@ -317,15 +319,43 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } } - public async Task Shutdown() + [LoggerMessage(100, LogLevel.Error, "Error running handler")] + partial void ErrorRunningHandler(Exception exception); + + public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) { - this.EventChannel.Writer.Complete(); + await this._shutdownDelegate(cancellationToken).ConfigureAwait(false); + } - await this.EventChannel.Reader.Completion.ConfigureAwait(false); + internal void SetShutdownDelegate(ShutdownDelegate del) + { + this._shutdownDelegate = del; } - [LoggerMessage(100, LogLevel.Error, "Error running handler")] - partial void ErrorRunningHandler(Exception exception); + // Method to signal shutdown + private async ValueTask SignalShutdownAsync(CancellationToken cancellationToken) + { + // Enqueue a shutdown signal + await this.EventChannel.Writer.WriteAsync(new ShutdownSignal(), cancellationToken).ConfigureAwait(false); + + // Wait for the processing loop to acknowledge the shutdown + await this._shutdownSemaphore.WaitAsync(cancellationToken).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 diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index bcc66558..b7abe6f6 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using OpenFeature.Constant; @@ -43,9 +44,10 @@ public abstract class FeatureProvider /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a string feature flag @@ -53,9 +55,10 @@ public abstract Task> ResolveBooleanValue(string flagKey /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a integer feature flag @@ -63,9 +66,10 @@ public abstract Task> ResolveStringValue(string flagKe /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a double feature flag @@ -73,9 +77,10 @@ public abstract Task> ResolveIntegerValue(string flagKey, /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a structured feature flag @@ -83,9 +88,10 @@ public abstract Task> ResolveDoubleValue(string flagKe /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Get the status of the provider. @@ -95,7 +101,7 @@ public abstract Task> ResolveStructureValue(string flag /// If a provider does not override this method, then its status will be assumed to be /// . If a provider implements this method, and supports initialization, /// then it should start in the status . If the status is - /// , then the Api will call the when the + /// , then the Api will call the when the /// provider is set. /// public virtual ProviderStatus GetStatus() => ProviderStatus.Ready; @@ -107,6 +113,7 @@ public abstract Task> ResolveStructureValue(string flag /// /// /// + /// The . /// A task that completes when the initialization process is complete. /// /// @@ -118,10 +125,10 @@ public abstract Task> ResolveStructureValue(string flag /// the method after initialization is complete. /// /// - public virtual Task Initialize(EvaluationContext context) + public virtual ValueTask InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { // Intentionally left blank. - return Task.CompletedTask; + return new ValueTask(); } /// @@ -129,10 +136,11 @@ public virtual Task Initialize(EvaluationContext context) /// Providers can overwrite this method, if they have special shutdown actions needed. /// /// A task that completes when the shutdown process is complete. - public virtual Task Shutdown() + /// The . + public virtual ValueTask ShutdownAsync(CancellationToken cancellationToken = default) { // Intentionally left blank. - return Task.CompletedTask; + return new ValueTask(); } /// diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs index 50162729..aea5dc15 100644 --- a/src/OpenFeature/Hook.cs +++ b/src/OpenFeature/Hook.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Model; @@ -26,12 +27,13 @@ public abstract class Hook /// /// Provides context of innovation /// Caller provided data + /// The . /// Flag value type (bool|number|string|object) /// Modified EvaluationContext that is used for the flag evaluation - public virtual Task Before(HookContext context, - IReadOnlyDictionary? hints = null) + public virtual ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.FromResult(EvaluationContext.Empty); + return new ValueTask(EvaluationContext.Empty); } /// @@ -40,11 +42,12 @@ public virtual Task Before(HookContext context, /// Provides context of innovation /// Flag evaluation information /// Caller provided data + /// The . /// Flag value type (bool|number|string|object) - public virtual Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null) + public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } /// @@ -53,11 +56,12 @@ public virtual Task After(HookContext context, FlagEvaluationDetails de /// Provides context of innovation /// Exception representing what went wrong /// Caller provided data + /// The . /// Flag value type (bool|number|string|object) - public virtual Task Error(HookContext context, Exception error, - IReadOnlyDictionary? hints = null) + public virtual ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } /// @@ -65,10 +69,11 @@ public virtual Task Error(HookContext context, Exception error, /// /// Provides context of innovation /// Caller provided data + /// The . /// Flag value type (bool|number|string|object) - public virtual Task Finally(HookContext context, IReadOnlyDictionary? hints = null) + public virtual ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } } } diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index b262f8f1..4a09c5e8 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Model; @@ -59,8 +60,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a boolean feature flag @@ -69,8 +71,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a string feature flag @@ -79,8 +82,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a string feature flag @@ -89,8 +93,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetStringDetailsAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a integer feature flag @@ -99,8 +104,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a integer feature flag @@ -109,8 +115,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a double feature flag @@ -119,8 +126,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a double feature flag @@ -129,8 +137,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a structure object feature flag @@ -139,8 +148,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a structure object feature flag @@ -149,7 +159,8 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); } } diff --git a/src/OpenFeature/NoOpProvider.cs b/src/OpenFeature/NoOpProvider.cs index 693e504e..5d7b9caa 100644 --- a/src/OpenFeature/NoOpProvider.cs +++ b/src/OpenFeature/NoOpProvider.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; @@ -13,27 +14,27 @@ public override Metadata GetMetadata() return this._metadata; } - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null) + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext? context = null) + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null) + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext? context = null) + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 6145094e..9e32d78a 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -36,9 +37,9 @@ public sealed partial class FeatureClient : IFeatureClient /// /// The type of the resolution method /// A tuple containing a resolution method and the provider it came from. - private (Func>>, FeatureProvider) + private (Func>>, FeatureProvider) ExtractProvider( - Func>>> method) + Func>>> method) { // Alias the provider reference so getting the method and returning the provider are // guaranteed to be the same object. @@ -136,70 +137,71 @@ public void AddHooks(IEnumerable hooks) public void ClearHooks() => this._hooks.Clear(); /// - public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetBooleanDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetBooleanDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetBooleanDetails(string flagKey, bool defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue), + public async Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveBooleanValueAsync), FlagValueType.Boolean, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetStringDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetStringDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetStringDetails(string flagKey, string defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue), + public async Task> GetStringDetailsAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStringValueAsync), FlagValueType.String, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetIntegerDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetIntegerDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetIntegerDetails(string flagKey, int defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue), + public async Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveIntegerValueAsync), FlagValueType.Number, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetDoubleValue(string flagKey, double defaultValue, + public async Task GetDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetDoubleDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetDoubleDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetDoubleDetails(string flagKey, double defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue), + public async Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveDoubleValueAsync), FlagValueType.Number, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetObjectDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetObjectDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetObjectDetails(string flagKey, Value defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue), + public async Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStructureValueAsync), FlagValueType.Object, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); - private async Task> EvaluateFlag( - (Func>>, FeatureProvider) providerInfo, + private async Task> EvaluateFlagAsync( + (Func>>, FeatureProvider) providerInfo, FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? options = null) + FlagEvaluationOptions? options = null, + CancellationToken cancellationToken = default) { var resolveValueDelegate = providerInfo.Item1; var provider = providerInfo.Item2; @@ -242,45 +244,45 @@ private async Task> EvaluateFlag( FlagEvaluationDetails evaluation; try { - var contextFromHooks = await this.TriggerBeforeHooks(allHooks, hookContext, options).ConfigureAwait(false); + var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false); evaluation = - (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext).ConfigureAwait(false)) + (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false)) .ToFlagEvaluationDetails(); - await this.TriggerAfterHooks(allHooksReversed, hookContext, evaluation, options).ConfigureAwait(false); + await this.TriggerAfterHooksAsync(allHooksReversed, hookContext, evaluation, options, cancellationToken).ConfigureAwait(false); } catch (FeatureProviderException ex) { this.FlagEvaluationErrorWithDescription(flagKey, ex.ErrorType.GetDescription(), ex); evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error, string.Empty, ex.Message); - await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); + await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, ex, options, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { this.FlagEvaluationError(flagKey, ex); var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General; evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty, ex.Message); - await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); + await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, ex, options, cancellationToken).ConfigureAwait(false); } finally { - await this.TriggerFinallyHooks(allHooksReversed, hookContext, options).ConfigureAwait(false); + await this.TriggerFinallyHooksAsync(allHooksReversed, hookContext, options, cancellationToken).ConfigureAwait(false); } return evaluation; } - private async Task> TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions? options) + private async ValueTask> TriggerBeforeHooksAsync(IReadOnlyList hooks, HookContext context, + FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { var evalContextBuilder = EvaluationContext.Builder(); evalContextBuilder.Merge(context.EvaluationContext); foreach (var hook in hooks) { - var resp = await hook.Before(context, options?.HookHints).ConfigureAwait(false); + var resp = await hook.BeforeAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false); if (resp != null) { evalContextBuilder.Merge(resp); @@ -295,23 +297,23 @@ private async Task> TriggerBeforeHooks(IReadOnlyList hoo return context.WithNewEvaluationContext(evalContextBuilder.Build()); } - private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options) + private async ValueTask TriggerAfterHooksAsync(IReadOnlyList hooks, HookContext context, + FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) { - await hook.After(context, evaluationDetails, options?.HookHints).ConfigureAwait(false); + await hook.AfterAsync(context, evaluationDetails, options?.HookHints, cancellationToken).ConfigureAwait(false); } } - private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext context, Exception exception, - FlagEvaluationOptions? options) + private async ValueTask TriggerErrorHooksAsync(IReadOnlyList hooks, HookContext context, Exception exception, + FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) { try { - await hook.Error(context, exception, options?.HookHints).ConfigureAwait(false); + await hook.ErrorAsync(context, exception, options?.HookHints, cancellationToken).ConfigureAwait(false); } catch (Exception e) { @@ -320,14 +322,14 @@ private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions? options) + private async ValueTask TriggerFinallyHooksAsync(IReadOnlyList hooks, HookContext context, + FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) { try { - await hook.Finally(context, options?.HookHints).ConfigureAwait(false); + await hook.FinallyAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false); } catch (Exception e) { diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 5b331d43..8e756b25 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -35,7 +35,7 @@ public async ValueTask DisposeAsync() { using (this._providersLock) { - await this.Shutdown().ConfigureAwait(false); + await this.ShutdownAsync().ConfigureAwait(false); } } @@ -62,13 +62,15 @@ public async ValueTask DisposeAsync() /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - public async Task SetProvider( + /// The . + public async ValueTask SetProviderAsync( FeatureProvider? featureProvider, EvaluationContext context, Action? afterSet = null, Action? afterInitialization = null, Action? afterError = null, - Action? afterShutdown = null) + Action? afterShutdown = null, + CancellationToken cancellationToken = default) { // Cannot unset the feature provider. if (featureProvider == null) @@ -92,7 +94,7 @@ public async Task SetProvider( // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. #pragma warning disable CS4014 - this.ShutdownIfUnused(oldProvider, afterShutdown, afterError); + this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError, cancellationToken); #pragma warning restore CS4014 } finally @@ -100,15 +102,16 @@ public async Task SetProvider( this._providersLock.ExitWriteLock(); } - await InitProvider(this._defaultProvider, context, afterInitialization, afterError) + await InitProviderAsync(this._defaultProvider, context, afterInitialization, afterError, cancellationToken) .ConfigureAwait(false); } - private static async Task InitProvider( + private static async ValueTask InitProviderAsync( FeatureProvider? newProvider, EvaluationContext context, Action? afterInitialization, - Action? afterError) + Action? afterError, + CancellationToken cancellationToken) { if (newProvider == null) { @@ -118,7 +121,7 @@ private static async Task InitProvider( { try { - await newProvider.Initialize(context).ConfigureAwait(false); + await newProvider.InitializeAsync(context, cancellationToken).ConfigureAwait(false); afterInitialization?.Invoke(newProvider); } catch (Exception ex) @@ -152,13 +155,15 @@ private static async Task InitProvider( /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - public async Task SetProvider(string? clientName, + /// The . + public async ValueTask SetProviderAsync(string clientName, FeatureProvider? featureProvider, EvaluationContext context, Action? afterSet = null, Action? afterInitialization = null, Action? afterError = null, - Action? afterShutdown = null) + Action? afterShutdown = null, + CancellationToken cancellationToken = default) { // Cannot set a provider for a null clientName. if (clientName == null) @@ -187,7 +192,7 @@ public async Task SetProvider(string? clientName, // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. #pragma warning disable CS4014 - this.ShutdownIfUnused(oldProvider, afterShutdown, afterError); + this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError, cancellationToken); #pragma warning restore CS4014 } finally @@ -195,16 +200,17 @@ public async Task SetProvider(string? clientName, this._providersLock.ExitWriteLock(); } - await InitProvider(featureProvider, context, afterInitialization, afterError).ConfigureAwait(false); + await InitProviderAsync(featureProvider, context, afterInitialization, afterError, cancellationToken).ConfigureAwait(false); } /// /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. /// - private async Task ShutdownIfUnused( + private async ValueTask ShutdownIfUnusedAsync( FeatureProvider? targetProvider, Action? afterShutdown, - Action? afterError) + Action? afterError, + CancellationToken cancellationToken) { if (ReferenceEquals(this._defaultProvider, targetProvider)) { @@ -216,7 +222,7 @@ private async Task ShutdownIfUnused( return; } - await SafeShutdownProvider(targetProvider, afterShutdown, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider, afterShutdown, afterError, cancellationToken).ConfigureAwait(false); } /// @@ -228,9 +234,10 @@ private async Task ShutdownIfUnused( /// it would not be meaningful to emit an error. /// /// - private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, + private static async ValueTask SafeShutdownProviderAsync(FeatureProvider? targetProvider, Action? afterShutdown, - Action? afterError) + Action? afterError, + CancellationToken cancellationToken) { if (targetProvider == null) { @@ -239,7 +246,7 @@ private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, try { - await targetProvider.Shutdown().ConfigureAwait(false); + await targetProvider.ShutdownAsync(cancellationToken).ConfigureAwait(false); afterShutdown?.Invoke(targetProvider); } catch (Exception ex) @@ -281,7 +288,7 @@ public FeatureProvider GetProvider(string? clientName) : this.GetProvider(); } - public async Task Shutdown(Action? afterError = null) + public async ValueTask ShutdownAsync(Action? afterError = null, CancellationToken cancellationToken = default) { var providers = new HashSet(); this._providersLock.EnterWriteLock(); @@ -305,7 +312,7 @@ public async Task Shutdown(Action? afterError = null foreach (var targetProvider in providers) { // We don't need to take any actions after shutdown. - await SafeShutdownProvider(targetProvider, null, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider, null, afterError, cancellationToken).ConfigureAwait(false); } } } diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 03f6082a..7f2e5b30 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -45,62 +45,62 @@ public OpenFeatureClientBenchmarks() [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValue(_flagName, _defaultBoolValue); + await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValue(_flagName, _defaultBoolValue, EvaluationContext.Empty); + await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetBooleanValue(_flagName, _defaultBoolValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValue(_flagName, _defaultStringValue); + await _client.GetStringValueAsync(_flagName, _defaultStringValue); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValue(_flagName, _defaultStringValue, EvaluationContext.Empty); + await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetStringValue(_flagName, _defaultStringValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValue(_flagName, _defaultIntegerValue); + await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValue(_flagName, _defaultIntegerValue, EvaluationContext.Empty); + await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetIntegerValue(_flagName, _defaultIntegerValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValue(_flagName, _defaultDoubleValue); + await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValue(_flagName, _defaultDoubleValue, EvaluationContext.Empty); + await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetDoubleValue(_flagName, _defaultDoubleValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValue(_flagName, _defaultStructureValue); + await _client.GetObjectValueAsync(_flagName, _defaultStructureValue); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValue(_flagName, _defaultStructureValue, EvaluationContext.Empty); + await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetObjectValue(_flagName, _defaultStructureValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty, _emptyFlagOptions); } } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index b7b1f9b5..c066409e 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -49,7 +49,7 @@ public void GivenAProviderIsRegistered() [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagValue = client?.GetBooleanValue(flagKey, defaultValue); + this.booleanFlagValue = client?.GetBooleanValueAsync(flagKey, defaultValue); } [Then(@"the resolved boolean value should be ""(.*)""")] @@ -61,7 +61,7 @@ public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - this.stringFlagValue = client?.GetStringValue(flagKey, defaultValue); + this.stringFlagValue = client?.GetStringValueAsync(flagKey, defaultValue); } [Then(@"the resolved string value should be ""(.*)""")] @@ -73,7 +73,7 @@ public void Thentheresolvedstringvalueshouldbe(string expected) [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) { - this.intFlagValue = client?.GetIntegerValue(flagKey, defaultValue); + this.intFlagValue = client?.GetIntegerValueAsync(flagKey, defaultValue); } [Then(@"the resolved integer value should be (.*)")] @@ -85,7 +85,7 @@ public void Thentheresolvedintegervalueshouldbe(int expected) [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagValue = client?.GetDoubleValue(flagKey, defaultValue); + this.doubleFlagValue = client?.GetDoubleValueAsync(flagKey, defaultValue); } [Then(@"the resolved float value should be (.*)")] @@ -97,7 +97,7 @@ public void Thentheresolvedfloatvalueshouldbe(double expected) [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) { - this.objectFlagValue = client?.GetObjectValue(flagKey, new Value()); + this.objectFlagValue = client?.GetObjectValueAsync(flagKey, new Value()); } [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] @@ -112,7 +112,7 @@ public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespe [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagDetails = client?.GetBooleanDetails(flagKey, defaultValue); + this.booleanFlagDetails = client?.GetBooleanDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -127,7 +127,7 @@ public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthere [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) { - this.stringFlagDetails = client?.GetStringDetails(flagKey, defaultValue); + this.stringFlagDetails = client?.GetStringDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -142,7 +142,7 @@ public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandtherea [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) { - this.intFlagDetails = client?.GetIntegerDetails(flagKey, defaultValue); + this.intFlagDetails = client?.GetIntegerDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -157,7 +157,7 @@ public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthere [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagDetails = client?.GetDoubleDetails(flagKey, defaultValue); + this.doubleFlagDetails = client?.GetDoubleDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -172,7 +172,7 @@ public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereas [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) { - this.objectFlagDetails = client?.GetObjectDetails(flagKey, new Value()); + this.objectFlagDetails = client?.GetObjectDetailsAsync(flagKey, new Value()); } [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] @@ -206,7 +206,7 @@ public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string { contextAwareFlagKey = flagKey; contextAwareDefaultValue = defaultValue; - contextAwareValue = client?.GetStringValue(flagKey, contextAwareDefaultValue, context)?.Result; + contextAwareValue = client?.GetStringValueAsync(flagKey, contextAwareDefaultValue, context)?.Result; } [Then(@"the resolved string response should be ""(.*)""")] @@ -218,7 +218,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string? emptyContextValue = client?.GetStringValue(contextAwareFlagKey!, contextAwareDefaultValue!, new EvaluationContextBuilder().Build()).Result; + string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey, contextAwareDefaultValue, new EvaluationContext(new Structure(ImmutableDictionary.Empty))).Result; Assert.Equal(expected, emptyContextValue); } @@ -227,7 +227,7 @@ public void Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultva { this.notFoundFlagKey = flagKey; this.notFoundDefaultValue = defaultValue; - this.notFoundDetails = client?.GetStringDetails(this.notFoundFlagKey, this.notFoundDefaultValue).Result; + this.notFoundDetails = client?.GetStringDetailsAsync(this.notFoundFlagKey, this.notFoundDefaultValue).Result; } [Then(@"the default string value should be returned")] @@ -248,7 +248,7 @@ public void Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultva { this.typeErrorFlagKey = flagKey; this.typeErrorDefaultValue = defaultValue; - this.typeErrorDetails = client?.GetIntegerDetails(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; + this.typeErrorDetails = client?.GetIntegerDetailsAsync(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; } [Then(@"the default integer value should be returned")] diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index 8d679f94..53a67443 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -44,27 +44,27 @@ public async Task Provider_Must_Resolve_Flag_Values() var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveBooleanValue(flagName, defaultBoolValue)).Should() + (await provider.ResolveBooleanValueAsync(flagName, defaultBoolValue)).Should() .BeEquivalentTo(boolResolutionDetails); var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveIntegerValue(flagName, defaultIntegerValue)).Should() + (await provider.ResolveIntegerValueAsync(flagName, defaultIntegerValue)).Should() .BeEquivalentTo(integerResolutionDetails); var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveDoubleValue(flagName, defaultDoubleValue)).Should() + (await provider.ResolveDoubleValueAsync(flagName, defaultDoubleValue)).Should() .BeEquivalentTo(doubleResolutionDetails); var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStringValue(flagName, defaultStringValue)).Should() + (await provider.ResolveStringValueAsync(flagName, defaultStringValue)).Should() .BeEquivalentTo(stringResolutionDetails); var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStructureValue(flagName, defaultStructureValue)).Should() + (await provider.ResolveStructureValueAsync(flagName, defaultStructureValue)).Should() .BeEquivalentTo(structureResolutionDetails); } @@ -84,59 +84,59 @@ public async Task Provider_Must_ErrorType() var providerMock = Substitute.For(); const string testMessage = "An error message"; - providerMock.ResolveBooleanValue(flagName, defaultBoolValue, Arg.Any()) + providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveIntegerValue(flagName, defaultIntegerValue, Arg.Any()) + providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveDoubleValue(flagName, defaultDoubleValue, Arg.Any()) + providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.InvalidContext, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveStringValue(flagName, defaultStringValue, Arg.Any()) + providerMock.ResolveStringValueAsync(flagName, defaultStringValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveStructureValue(flagName, defaultStructureValue, Arg.Any()) + providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveStructureValue(flagName2, defaultStructureValue, Arg.Any()) + providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue, Arg.Any()) .Returns(new ResolutionDetails(flagName2, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveBooleanValue(flagName2, defaultBoolValue, Arg.Any()) + providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue, Arg.Any()) .Returns(new ResolutionDetails(flagName2, defaultBoolValue, ErrorType.TargetingKeyMissing, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - var boolRes = await providerMock.ResolveBooleanValue(flagName, defaultBoolValue); + var boolRes = await providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue); boolRes.ErrorType.Should().Be(ErrorType.General); boolRes.ErrorMessage.Should().Be(testMessage); - var intRes = await providerMock.ResolveIntegerValue(flagName, defaultIntegerValue); + var intRes = await providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue); intRes.ErrorType.Should().Be(ErrorType.ParseError); intRes.ErrorMessage.Should().Be(testMessage); - var doubleRes = await providerMock.ResolveDoubleValue(flagName, defaultDoubleValue); + var doubleRes = await providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue); doubleRes.ErrorType.Should().Be(ErrorType.InvalidContext); doubleRes.ErrorMessage.Should().Be(testMessage); - var stringRes = await providerMock.ResolveStringValue(flagName, defaultStringValue); + var stringRes = await providerMock.ResolveStringValueAsync(flagName, defaultStringValue); stringRes.ErrorType.Should().Be(ErrorType.TypeMismatch); stringRes.ErrorMessage.Should().Be(testMessage); - var structRes1 = await providerMock.ResolveStructureValue(flagName, defaultStructureValue); + var structRes1 = await providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue); structRes1.ErrorType.Should().Be(ErrorType.FlagNotFound); structRes1.ErrorMessage.Should().Be(testMessage); - var structRes2 = await providerMock.ResolveStructureValue(flagName2, defaultStructureValue); + var structRes2 = await providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue); structRes2.ErrorType.Should().Be(ErrorType.ProviderNotReady); structRes2.ErrorMessage.Should().Be(testMessage); - var boolRes2 = await providerMock.ResolveBooleanValue(flagName2, defaultBoolValue); + var boolRes2 = await providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue); boolRes2.ErrorType.Should().Be(ErrorType.TargetingKeyMissing); boolRes2.ErrorMessage.Should().BeNull(); } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 7c656cec..ad90bb51 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -78,25 +78,25 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetBooleanValue(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); - (await client.GetBooleanValue(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); - (await client.GetBooleanValue(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultBoolValue); + (await client.GetBooleanValueAsync(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); + (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); + (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultBoolValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue)).Should().Be(defaultIntegerValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().Be(defaultIntegerValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValueAsync(flagName, defaultIntegerValue)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultIntegerValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue)).Should().Be(defaultDoubleValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().Be(defaultDoubleValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValueAsync(flagName, defaultDoubleValue)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultDoubleValue); - (await client.GetStringValue(flagName, defaultStringValue)).Should().Be(defaultStringValue); - (await client.GetStringValue(flagName, defaultStringValue, EvaluationContext.Empty)).Should().Be(defaultStringValue); - (await client.GetStringValue(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultStringValue); + (await client.GetStringValueAsync(flagName, defaultStringValue)).Should().Be(defaultStringValue); + (await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty)).Should().Be(defaultStringValue); + (await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultStringValue); - (await client.GetObjectValue(flagName, defaultStructureValue)).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValue(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValue(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValueAsync(flagName, defaultStructureValue)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(defaultStructureValue); } [Fact] @@ -125,29 +125,29 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var client = Api.Instance.GetClient(clientName, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetBooleanDetails(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetails(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetails(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); var integerFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetIntegerDetails(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetails(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetails(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); var doubleFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetDoubleDetails(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetails(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetails(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); var stringFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetStringDetails(flagName, defaultStringValue)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetails(flagName, defaultStringValue, EvaluationContext.Empty)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetails(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetailsAsync(flagName, defaultStringValue)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetObjectDetails(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetails(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetails(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetailsAsync(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); } [Fact] @@ -168,18 +168,18 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var mockedLogger = Substitute.For>(); // This will fail to case a String to TestStructure - mockedFeatureProvider.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Throws(); + mockedFeatureProvider.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(); mockedFeatureProvider.GetMetadata().Returns(new Metadata(fixture.Create())); mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(mockedFeatureProvider); var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger); - var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); + var evaluationDetails = await client.GetObjectDetailsAsync(flagName, defaultValue); evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); evaluationDetails.ErrorMessage.Should().Be(new InvalidCastException().Message); - _ = mockedFeatureProvider.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = mockedFeatureProvider.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); mockedLogger.Received(1).IsEnabled(LogLevel.Error); } @@ -194,16 +194,16 @@ public async Task Should_Resolve_BooleanValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveBooleanValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetBooleanValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetBooleanValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveBooleanValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -216,16 +216,16 @@ public async Task Should_Resolve_StringValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStringValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveStringValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetStringValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetStringValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveStringValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -238,16 +238,16 @@ public async Task Should_Resolve_IntegerValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveIntegerValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetIntegerValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveIntegerValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -260,16 +260,16 @@ public async Task Should_Resolve_DoubleValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveDoubleValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetDoubleValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveDoubleValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -282,16 +282,16 @@ public async Task Should_Resolve_StructureValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetObjectValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -305,18 +305,18 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() const string testMessage = "Couldn't parse flag data."; var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, "ERROR", null, testMessage))); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, "ERROR", null, testMessage))); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - var response = await client.GetObjectDetails(flagName, defaultValue); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); response.Reason.Should().Be(Reason.Error); response.ErrorMessage.Should().Be(testMessage); - _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -330,18 +330,18 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() const string testMessage = "Couldn't parse flag data."; var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - var response = await client.GetObjectDetails(flagName, defaultValue); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); response.Reason.Should().Be(Reason.Error); response.ErrorMessage.Should().Be(testMessage); - _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -349,7 +349,7 @@ public async Task Should_Use_No_Op_When_Provider_Is_Null() { await Api.Instance.SetProviderAsync(null); var client = new FeatureClient("test", "test"); - (await client.GetIntegerValue("some-key", 12)).Should().Be(12); + (await client.GetIntegerValueAsync("some-key", 12)).Should().Be(12); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 3a373c98..2502bd8c 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -46,7 +46,7 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() eventHandler.Received().Invoke(Arg.Is(payload => payload == myEvent.EventPayload)); // shut down the event executor - await eventExecutor.Shutdown(); + await eventExecutor.ShutdownAsync(); // the next event should not be propagated to the event handler var newEventPayload = new ProviderEventPayload @@ -78,9 +78,9 @@ public async Task API_Level_Event_Handlers_Should_Be_Registered() var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); - testProvider.SendEvent(ProviderEventTypes.ProviderError); - testProvider.SendEvent(ProviderEventTypes.ProviderStale); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderError); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderStale); await Utils.AssertUntilAsync(_ => eventHandler .Received() @@ -228,12 +228,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); var newTestProvider = new TestProvider(); await Api.Instance.SetProviderAsync(newTestProvider); - newTestProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await newTestProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); await Utils.AssertUntilAsync( _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)) @@ -407,7 +407,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler); - defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); // verify that the client received the event from the default provider as there is no named provider registered yet await Utils.AssertUntilAsync( @@ -419,8 +419,8 @@ await Utils.AssertUntilAsync( await Api.Instance.SetProviderAsync(client.GetMetadata().Name!, clientProvider); // now, send another event for the default handler - defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); - clientProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + await clientProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); // now the client should have received only the event from the named provider await Utils.AssertUntilAsync( @@ -479,7 +479,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); // send another event from the provider - this one should not be received - testProvider.SendEvent(ProviderEventTypes.ProviderReady); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderReady); // wait a bit and make sure we only have received the first event, but nothing after removing the event handler await Utils.AssertUntilAsync( diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index b4cb958c..0e3effc1 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -35,18 +35,18 @@ public async Task Hooks_Should_Be_Called_In_Order() var providerHook = Substitute.For(); // Sequence - apiHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - clientHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - invocationHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - providerHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - providerHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - invocationHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - clientHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - apiHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - providerHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - invocationHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - clientHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - apiHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); var testProvider = new TestProvider(); testProvider.AddHook(providerHook); @@ -55,37 +55,37 @@ public async Task Hooks_Should_Be_Called_In_Order() var client = Api.Instance.GetClient(clientName, clientVersion); client.AddHooks(clientHook); - await client.GetBooleanValue(flagName, defaultValue, EvaluationContext.Empty, + await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empty, new FlagEvaluationOptions(invocationHook, ImmutableDictionary.Empty)); Received.InOrder(() => { - apiHook.Before(Arg.Any>(), Arg.Any>()); - clientHook.Before(Arg.Any>(), Arg.Any>()); - invocationHook.Before(Arg.Any>(), Arg.Any>()); - providerHook.Before(Arg.Any>(), Arg.Any>()); - providerHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - invocationHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - clientHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - apiHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - providerHook.Finally(Arg.Any>(), Arg.Any>()); - invocationHook.Finally(Arg.Any>(), Arg.Any>()); - clientHook.Finally(Arg.Any>(), Arg.Any>()); - apiHook.Finally(Arg.Any>(), Arg.Any>()); + apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>()); }); - _ = apiHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = apiHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = apiHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); } [Fact] @@ -139,15 +139,15 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName), evaluationContext); - hook1.Before(Arg.Any>(), Arg.Any>()).Returns(evaluationContext); - hook2.Before(hookContext, Arg.Any>()).Returns(evaluationContext); + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(evaluationContext); + hook2.BeforeAsync(hookContext, Arg.Any>()).Returns(evaluationContext); var client = Api.Instance.GetClient("test", "1.0.0"); - await client.GetBooleanValue("test", false, EvaluationContext.Empty, + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, new FlagEvaluationOptions(ImmutableList.Create(hook1, hook2), ImmutableDictionary.Empty)); - _ = hook1.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = hook2.Received(1).Before(Arg.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), Arg.Any>()); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook2.Received(1).BeforeAsync(Arg.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), Arg.Any>()); } [Fact] @@ -195,19 +195,19 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() provider.GetProviderHooks().Returns(ImmutableList.Empty); - provider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); + provider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); await Api.Instance.SetProviderAsync(provider); var hook = Substitute.For(); - hook.Before(Arg.Any>(), Arg.Any>()).Returns(hookContext); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(hookContext); var client = Api.Instance.GetClient("test", "1.0.0", null, clientContext); - await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty)); + await client.GetBooleanValueAsync("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty)); // after proper merging, all properties should equal true - _ = provider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Is(y => + _ = provider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Is(y => (y.GetValue(propGlobal).AsBoolean ?? false) && (y.GetValue(propClient).AsBoolean ?? false) && (y.GetValue(propGlobalToOverwrite).AsBoolean ?? false) @@ -238,10 +238,10 @@ public async Task Hook_Should_Return_No_Errors() var hookContext = new HookContext("test", false, FlagValueType.Boolean, new ClientMetadata(null, null), new Metadata(null), EvaluationContext.Empty); - await hook.Before(hookContext, hookHints); - await hook.After(hookContext, new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"), hookHints); - await hook.Finally(hookContext, hookHints); - await hook.Error(hookContext, new Exception(), hookHints); + await hook.BeforeAsync(hookContext, hookHints); + await hook.AfterAsync(hookContext, new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"), hookHints); + await hook.FinallyAsync(hookContext, hookHints); + await hook.ErrorAsync(hookContext, new Exception(), hookHints); hookContext.ClientMetadata.Name.Should().BeNull(); hookContext.ClientMetadata.Version.Should().BeNull(); @@ -264,29 +264,29 @@ public async Task Hook_Should_Execute_In_Correct_Order() featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); - _ = hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.Finally(Arg.Any>(), Arg.Any>()); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook.Before(Arg.Any>(), Arg.Any>()); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Finally(Arg.Any>(), Arg.Any>()); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.FinallyAsync(Arg.Any>(), Arg.Any>()); }); - _ = hook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = hook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = featureProvider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -304,7 +304,7 @@ public async Task Register_Hooks_Should_Be_Available_At_All_Levels() await Api.Instance.SetProviderAsync(testProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook2); - await client.GetBooleanValue("test", false, null, + await client.GetBooleanValueAsync("test", false, null, new FlagEvaluationOptions(hook3, ImmutableDictionary.Empty)); Assert.Single(Api.Instance.GetHooks()); @@ -324,39 +324,39 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook1.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - hook2.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); - hook2.After(Arg.Any>(), Arg.Any>(), null).Returns(Task.CompletedTask); - hook1.After(Arg.Any>(), Arg.Any>(), null).Returns(Task.CompletedTask); - hook2.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); - hook1.Finally(Arg.Any>(), null).Throws(new Exception()); + hook1.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + hook2.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook2.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask()); + hook1.FinallyAsync(Arg.Any>(), null).Throws(new Exception()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); client.GetHooks().Count().Should().Be(2); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook1.Before(Arg.Any>(), null); - hook2.Before(Arg.Any>(), null); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - hook2.After(Arg.Any>(), Arg.Any>(), null); - hook1.After(Arg.Any>(), Arg.Any>(), null); - hook2.Finally(Arg.Any>(), null); - hook1.Finally(Arg.Any>(), null); + hook1.BeforeAsync(Arg.Any>(), null); + hook2.BeforeAsync(Arg.Any>(), null); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null); + hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null); + hook2.FinallyAsync(Arg.Any>(), null); + hook1.FinallyAsync(Arg.Any>(), null); }); - _ = hook1.Received(1).Before(Arg.Any>(), null); - _ = hook2.Received(1).Before(Arg.Any>(), null); - _ = featureProvider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - _ = hook2.Received(1).After(Arg.Any>(), Arg.Any>(), null); - _ = hook1.Received(1).After(Arg.Any>(), Arg.Any>(), null); - _ = hook2.Received(1).Finally(Arg.Any>(), null); - _ = hook1.Received(1).Finally(Arg.Any>(), null); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); + _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + _ = hook2.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook1.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook2.Received(1).FinallyAsync(Arg.Any>(), null); + _ = hook1.Received(1).FinallyAsync(Arg.Any>(), null); } [Fact] @@ -371,31 +371,31 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() featureProvider1.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook1.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - hook2.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - featureProvider1.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Throws(new Exception()); - hook2.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); - hook1.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); + hook1.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + hook2.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + featureProvider1.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Throws(new Exception()); + hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider1); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook1.Before(Arg.Any>(), null); - hook2.Before(Arg.Any>(), null); - featureProvider1.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - hook2.Error(Arg.Any>(), Arg.Any(), null); - hook1.Error(Arg.Any>(), Arg.Any(), null); + hook1.BeforeAsync(Arg.Any>(), null); + hook2.BeforeAsync(Arg.Any>(), null); + featureProvider1.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); }); - _ = hook1.Received(1).Before(Arg.Any>(), null); - _ = hook2.Received(1).Before(Arg.Any>(), null); - _ = hook1.Received(1).Error(Arg.Any>(), Arg.Any(), null); - _ = hook2.Received(1).Error(Arg.Any>(), Arg.Any(), null); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); } [Fact] @@ -410,27 +410,27 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook1.Before(Arg.Any>(), Arg.Any>()).ThrowsAsync(new Exception()); - _ = hook1.Error(Arg.Any>(), Arg.Any(), null); - _ = hook2.Error(Arg.Any>(), Arg.Any(), null); + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(new Exception()); + _ = hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook1.Before(Arg.Any>(), Arg.Any>()); - hook2.Error(Arg.Any>(), Arg.Any(), null); - hook1.Error(Arg.Any>(), Arg.Any(), null); + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()); + hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); }); - _ = hook1.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = hook2.DidNotReceive().Before(Arg.Any>(), Arg.Any>()); - _ = hook1.Received(1).Error(Arg.Any>(), Arg.Any(), null); - _ = hook2.Received(1).Error(Arg.Any>(), Arg.Any(), null); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook2.DidNotReceive().BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); } [Fact] @@ -447,29 +447,29 @@ public async Task Hook_Hints_May_Be_Optional() featureProvider.GetProviderHooks() .Returns(ImmutableList.Empty); - hook.Before(Arg.Any>(), Arg.Any>()) + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) .Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue("test", false, Arg.Any()) + featureProvider.ResolveBooleanValueAsync("test", false, Arg.Any()) .Returns(new ResolutionDetails("test", false)); - hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .Returns(Task.FromResult(Task.CompletedTask)); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); - hook.Finally(Arg.Any>(), Arg.Any>()) - .Returns(Task.CompletedTask); + hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); - await client.GetBooleanValue("test", false, EvaluationContext.Empty, flagOptions); + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, flagOptions); Received.InOrder(() => { - hook.Received().Before(Arg.Any>(), Arg.Any>()); - featureProvider.Received().ResolveBooleanValue("test", false, Arg.Any()); - hook.Received().After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received().Finally(Arg.Any>(), Arg.Any>()); + hook.Received().BeforeAsync(Arg.Any>(), Arg.Any>()); + featureProvider.Received().ResolveBooleanValueAsync("test", false, Arg.Any()); + hook.Received().AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>()); }); } @@ -485,26 +485,26 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() featureProvider.GetMetadata().Returns(new Metadata(null)); // Sequence - hook.Before(Arg.Any>(), Arg.Any>()).ThrowsAsync(exceptionToThrow); - hook.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); - hook.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(exceptionToThrow); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); + hook.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask()); var client = Api.Instance.GetClient(); client.AddHooks(hook); - var resolvedFlag = await client.GetBooleanValue("test", true); + var resolvedFlag = await client.GetBooleanValueAsync("test", true); Received.InOrder(() => { - hook.Before(Arg.Any>(), Arg.Any>()); - hook.Error(Arg.Any>(), Arg.Any(), null); - hook.Finally(Arg.Any>(), null); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook.FinallyAsync(Arg.Any>(), null); }); resolvedFlag.Should().BeTrue(); - _ = hook.Received(1).Before(Arg.Any>(), null); - _ = hook.Received(1).Error(Arg.Any>(), exceptionToThrow, null); - _ = hook.Received(1).Finally(Arg.Any>(), null); + _ = hook.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook.Received(1).ErrorAsync(Arg.Any>(), exceptionToThrow, null); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), null); } [Fact] @@ -522,36 +522,36 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() featureProvider.GetProviderHooks() .Returns(ImmutableList.Empty); - hook.Before(Arg.Any>(), Arg.Any>()) + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) .Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()) + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new ResolutionDetails("test", false)); - hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .ThrowsAsync(exceptionToThrow); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Throws(exceptionToThrow); - hook.Error(Arg.Any>(), Arg.Any(), Arg.Any>()) - .Returns(Task.CompletedTask); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) + .Returns(new ValueTask()); - hook.Finally(Arg.Any>(), Arg.Any>()) - .Returns(Task.CompletedTask); + hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); - var resolvedFlag = await client.GetBooleanValue("test", true, config: flagOptions); + var resolvedFlag = await client.GetBooleanValueAsync("test", true, config: flagOptions); resolvedFlag.Should().BeTrue(); Received.InOrder(() => { - hook.Received(1).Before(Arg.Any>(), Arg.Any>()); - hook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); }); - await featureProvider.DidNotReceive().ResolveBooleanValue("test", false, Arg.Any()); + await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index c34a013d..99c4db61 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using NSubstitute; @@ -14,6 +15,8 @@ namespace OpenFeature.Tests [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { + static ValueTask EmptyShutdown(CancellationToken cancellationToken) => new ValueTask(); + [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() @@ -31,14 +34,14 @@ public async Task OpenFeature_Should_Initialize_Provider() var providerMockDefault = Substitute.For(); providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerMockDefault); - await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()); + await Api.Instance.SetProviderAsync(providerMockDefault).ConfigureAwait(false); + await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); var providerMockNamed = Substitute.For(); providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("the-name", providerMockNamed); - await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext()); + await Api.Instance.SetProviderAsync("the-name", providerMockNamed).ConfigureAwait(false); + await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); } [Fact] @@ -49,28 +52,28 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() var providerA = Substitute.For(); providerA.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerA); - await providerA.Received(1).Initialize(Api.Instance.GetContext()); + await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); + await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerB); - await providerB.Received(1).Initialize(Api.Instance.GetContext()); - await providerA.Received(1).Shutdown(); + await Api.Instance.SetProviderAsync(providerB).ConfigureAwait(false); + await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); + await providerA.Received(1).ShutdownAsync().ConfigureAwait(false); var providerC = Substitute.For(); providerC.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("named", providerC); - await providerC.Received(1).Initialize(Api.Instance.GetContext()); + await Api.Instance.SetProviderAsync("named", providerC).ConfigureAwait(false); + await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); var providerD = Substitute.For(); providerD.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("named", providerD); - await providerD.Received(1).Initialize(Api.Instance.GetContext()); - await providerC.Received(1).Shutdown(); + await Api.Instance.SetProviderAsync("named", providerD).ConfigureAwait(false); + await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); + await providerC.Received(1).ShutdownAsync().ConfigureAwait(false); } [Fact] @@ -83,13 +86,13 @@ public async Task OpenFeature_Should_Support_Shutdown() var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerA); - await Api.Instance.SetProviderAsync("named", providerB); + await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerB).ConfigureAwait(false); - await Api.Instance.Shutdown(); + await Api.Instance.ShutdownAsync().ConfigureAwait(false); - await providerA.Received(1).Shutdown(); - await providerB.Received(1).Shutdown(); + await providerA.Received(1).ShutdownAsync().ConfigureAwait(false); + await providerB.Received(1).ShutdownAsync().ConfigureAwait(false); } [Fact] @@ -98,8 +101,8 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def { var openFeature = Api.Instance; - await openFeature.SetProviderAsync(new NoOpFeatureProvider()); - await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()); + await openFeature.SetProviderAsync(new NoOpFeatureProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()).ConfigureAwait(false); var defaultClient = openFeature.GetProviderMetadata(); var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); @@ -114,7 +117,7 @@ public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() { var openFeature = Api.Instance; - await openFeature.SetProviderAsync(new TestProvider()); + await openFeature.SetProviderAsync(new TestProvider()).ConfigureAwait(false); var defaultClient = openFeature.GetProviderMetadata(); @@ -128,8 +131,8 @@ public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() const string name = "new-client"; var openFeature = Api.Instance; - await openFeature.SetProviderAsync(name, new TestProvider()).ConfigureAwait(true); - await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()).ConfigureAwait(true); + await openFeature.SetProviderAsync(name, new TestProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()).ConfigureAwait(false); openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); } @@ -141,8 +144,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instanc var openFeature = Api.Instance; var provider = new TestProvider(); - await openFeature.SetProviderAsync("a", provider).ConfigureAwait(true); - await openFeature.SetProviderAsync("b", provider).ConfigureAwait(true); + await openFeature.SetProviderAsync("a", provider).ConfigureAwait(false); + await openFeature.SetProviderAsync("b", provider).ConfigureAwait(false); var clientA = openFeature.GetProvider("a"); var clientB = openFeature.GetProvider("b"); @@ -183,7 +186,7 @@ public void OpenFeature_Should_Add_Hooks() [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] public async Task OpenFeature_Should_Get_Metadata() { - await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); + Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).GetAwaiter().GetResult(); var openFeature = Api.Instance; var metadata = openFeature.GetProviderMetadata(); @@ -233,8 +236,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() { var openFeature = Api.Instance; - await openFeature.SetProviderAsync("client1", new TestProvider()).ConfigureAwait(true); - await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()).ConfigureAwait(true); + await openFeature.SetProviderAsync("client1", new TestProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()).ConfigureAwait(false); var client1 = openFeature.GetClient("client1"); var client2 = openFeature.GetClient("client2"); @@ -242,19 +245,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() client1.GetMetadata().Name.Should().Be("client1"); client2.GetMetadata().Name.Should().Be("client2"); - (await client1.GetBooleanValue("test", false)).Should().BeTrue(); - (await client2.GetBooleanValue("test", false)).Should().BeFalse(); - } - - [Fact] - public async Task SetProviderAsync_Should_Throw_When_Null_ClientName() - { - var openFeature = Api.Instance; - - var exception = await Assert.ThrowsAsync(() => openFeature.SetProviderAsync(null!, new TestProvider())); - - exception.Should().BeOfType(); - exception.ParamName.Should().Be("clientName"); + client1.GetBooleanValueAsync("test", false).Result.Should().BeTrue(); + client2.GetBooleanValueAsync("test", false).Result.Should().BeFalse(); } } } diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 0b25ebfa..4178e595 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -21,7 +21,8 @@ public async Task Default_Provider_Is_Set_Without_Await() var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider, context); + // TODO: huh?? await?? + repository.SetProviderAsync(provider, context); Assert.Equal(provider, repository.GetProvider()); } @@ -33,7 +34,8 @@ public async void AfterSet_Is_Invoked_For_Setting_Default_Provider() var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProvider(provider, context, afterSet: (theProvider) => + // TODO: huh?? await?? + repository.SetProviderAsync(provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -48,9 +50,9 @@ public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Default_ var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(providerMock, context); - providerMock.Received(1).Initialize(context); - providerMock.DidNotReceive().Shutdown(); + await repository.SetProviderAsync(providerMock, context); + providerMock.Received(1).InitializeAsync(context); + providerMock.DidNotReceive().ShutdownAsync(); } [Fact] @@ -61,7 +63,7 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Default_Provider() providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider(providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync(providerMock, context, afterInitialization: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -76,10 +78,10 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provide var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); + providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; Exception? receivedError = null; - await repository.SetProvider(providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync(providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -99,8 +101,8 @@ public async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus sta var providerMock = Substitute.For(); providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(providerMock, context); - providerMock.DidNotReceive().Initialize(context); + await repository.SetProviderAsync(providerMock, context); + providerMock.DidNotReceive().InitializeAsync(context); } [Theory] @@ -114,7 +116,7 @@ public async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatu providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider(providerMock, context, afterInitialization: provider => { callCount++; }); + await repository.SetProviderAsync(providerMock, context, afterInitialization: provider => { callCount++; }); Assert.Equal(0, callCount); } @@ -129,10 +131,10 @@ public async Task Replaced_Default_Provider_Is_Shutdown() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider(provider2, context); - provider1.Received(1).Shutdown(); - provider2.DidNotReceive().Shutdown(); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync(provider2, context); + provider1.Received(1).ShutdownAsync(); + provider2.DidNotReceive().ShutdownAsync(); } [Fact] @@ -146,9 +148,9 @@ public async Task AfterShutdown_Is_Called_For_Shutdown_Provider() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); + await repository.SetProviderAsync(provider1, context); var callCount = 0; - await repository.SetProvider(provider2, context, afterShutdown: provider => + await repository.SetProviderAsync(provider2, context, afterShutdown: provider => { Assert.Equal(provider, provider1); callCount++; @@ -161,17 +163,17 @@ public async Task AfterError_Is_Called_For_Shutdown_That_Throws() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR")); + provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); provider1.GetStatus().Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); + await repository.SetProviderAsync(provider1, context); var callCount = 0; Exception? errorThrown = null; - await repository.SetProvider(provider2, context, afterError: (provider, ex) => + await repository.SetProviderAsync(provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); errorThrown = ex; @@ -187,7 +189,9 @@ public async Task Named_Provider_Provider_Is_Set_Without_Await() var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", provider, context); + // TODO: huh?? await?? + + repository.SetProviderAsync("the-name", provider, context); Assert.Equal(provider, repository.GetProvider("the-name")); } @@ -199,7 +203,8 @@ public async Task AfterSet_Is_Invoked_For_Setting_Named_Provider() var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProvider("the-name", provider, context, afterSet: (theProvider) => + // TODO: huh?? await?? + repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -214,9 +219,9 @@ public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Named_Pr var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", providerMock, context); - providerMock.Received(1).Initialize(context); - providerMock.DidNotReceive().Shutdown(); + await repository.SetProviderAsync("the-name", providerMock, context); + providerMock.Received(1).InitializeAsync(context); + providerMock.DidNotReceive().ShutdownAsync(); } [Fact] @@ -227,7 +232,7 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Named_Provider() providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider("the-name", providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync("the-name", providerMock, context, afterInitialization: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -242,10 +247,11 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider( var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); + providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; + // TODO: huh?? await?? Exception? receivedError = null; - await repository.SetProvider("the-provider", providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync("the-provider", providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -265,8 +271,8 @@ public async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStat var providerMock = Substitute.For(); providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", providerMock, context); - providerMock.DidNotReceive().Initialize(context); + await repository.SetProviderAsync("the-name", providerMock, context); + providerMock.DidNotReceive().InitializeAsync(context); } [Theory] @@ -280,7 +286,7 @@ public async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(Provide providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider("the-name", providerMock, context, + await repository.SetProviderAsync("the-name", providerMock, context, afterInitialization: provider => { callCount++; }); Assert.Equal(0, callCount); } @@ -296,10 +302,10 @@ public async Task Replaced_Named_Provider_Is_Shutdown() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", provider1, context); - await repository.SetProvider("the-name", provider2, context); - provider1.Received(1).Shutdown(); - provider2.DidNotReceive().Shutdown(); + await repository.SetProviderAsync("the-name", provider1, context); + await repository.SetProviderAsync("the-name", provider2, context); + provider1.Received(1).ShutdownAsync(); + provider2.DidNotReceive().ShutdownAsync(); } [Fact] @@ -313,9 +319,9 @@ public async Task AfterShutdown_Is_Called_For_Shutdown_Named_Provider() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-provider", provider1, context); + await repository.SetProviderAsync("the-provider", provider1, context); var callCount = 0; - await repository.SetProvider("the-provider", provider2, context, afterShutdown: provider => + await repository.SetProviderAsync("the-provider", provider2, context, afterShutdown: provider => { Assert.Equal(provider, provider1); callCount++; @@ -328,17 +334,18 @@ public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR")); + provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); provider1.GetStatus().Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", provider1, context); + await repository.SetProviderAsync("the-name", provider1, context); var callCount = 0; + // TODO: huh?? await?? Exception? errorThrown = null; - await repository.SetProvider("the-name", provider2, context, afterError: (provider, ex) => + await repository.SetProviderAsync("the-name", provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); errorThrown = ex; @@ -360,12 +367,12 @@ public async Task In_Use_Provider_Named_And_Default_Is_Not_Shutdown() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider("A", provider1, context); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("A", provider1, context); // Provider one is replaced for "A", but not default. - await repository.SetProvider("A", provider2, context); + await repository.SetProviderAsync("A", provider2, context); - provider1.DidNotReceive().Shutdown(); + provider1.DidNotReceive().ShutdownAsync(); } [Fact] @@ -380,12 +387,12 @@ public async Task In_Use_Provider_Two_Named_Is_Not_Shutdown() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("B", provider1, context); - await repository.SetProvider("A", provider1, context); + await repository.SetProviderAsync("B", provider1, context); + await repository.SetProviderAsync("A", provider1, context); // Provider one is replaced for "A", but not "B". - await repository.SetProvider("A", provider2, context); + await repository.SetProviderAsync("A", provider2, context); - provider1.DidNotReceive().Shutdown(); + provider1.DidNotReceive().ShutdownAsync(); } [Fact] @@ -400,13 +407,13 @@ public async Task When_All_Instances_Are_Removed_Shutdown_Is_Called() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("B", provider1, context); - await repository.SetProvider("A", provider1, context); + await repository.SetProviderAsync("B", provider1, context); + await repository.SetProviderAsync("A", provider1, context); - await repository.SetProvider("A", provider2, context); - await repository.SetProvider("B", provider2, context); + await repository.SetProviderAsync("A", provider2, context); + await repository.SetProviderAsync("B", provider2, context); - provider1.Received(1).Shutdown(); + provider1.Received(1).ShutdownAsync(); } [Fact] @@ -421,8 +428,8 @@ public async Task Can_Get_Providers_By_Name() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("A", provider1, context); - await repository.SetProvider("B", provider2, context); + await repository.SetProviderAsync("A", provider1, context); + await repository.SetProviderAsync("B", provider2, context); Assert.Equal(provider1, repository.GetProvider("A")); Assert.Equal(provider2, repository.GetProvider("B")); @@ -440,8 +447,8 @@ public async Task Replaced_Named_Provider_Gets_Latest_Set() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("A", provider1, context); - await repository.SetProvider("A", provider2, context); + await repository.SetProviderAsync("A", provider1, context); + await repository.SetProviderAsync("A", provider2, context); Assert.Equal(provider2, repository.GetProvider("A")); } @@ -461,17 +468,17 @@ public async Task Can_Shutdown_All_Providers() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider("provider1", provider1, context); - await repository.SetProvider("provider2", provider2, context); - await repository.SetProvider("provider2a", provider2, context); - await repository.SetProvider("provider3", provider3, context); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("provider1", provider1, context); + await repository.SetProviderAsync("provider2", provider2, context); + await repository.SetProviderAsync("provider2a", provider2, context); + await repository.SetProviderAsync("provider3", provider3, context); - await repository.Shutdown(); + await repository.ShutdownAsync(); - provider1.Received(1).Shutdown(); - provider2.Received(1).Shutdown(); - provider3.Received(1).Shutdown(); + provider1.Received(1).ShutdownAsync(); + provider2.Received(1).ShutdownAsync(); + provider3.Received(1).ShutdownAsync(); } [Fact] @@ -480,27 +487,27 @@ public async Task Errors_During_Shutdown_Propagate() var repository = new ProviderRepository(); var provider1 = Substitute.For(); provider1.GetStatus().Returns(ProviderStatus.NotReady); - provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR 1")); + provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 1")); var provider2 = Substitute.For(); provider2.GetStatus().Returns(ProviderStatus.NotReady); - provider2.Shutdown().Throws(new Exception("SHUTDOWN ERROR 2")); + provider2.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 2")); var provider3 = Substitute.For(); provider3.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider("provider1", provider1, context); - await repository.SetProvider("provider2", provider2, context); - await repository.SetProvider("provider2a", provider2, context); - await repository.SetProvider("provider3", provider3, context); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("provider1", provider1, context); + await repository.SetProviderAsync("provider2", provider2, context); + await repository.SetProviderAsync("provider2a", provider2, context); + await repository.SetProviderAsync("provider3", provider3, context); var callCountShutdown1 = 0; var callCountShutdown2 = 0; var totalCallCount = 0; - await repository.Shutdown(afterError: (provider, exception) => + await repository.ShutdownAsync(afterError: (provider, exception) => { totalCallCount++; if (provider == provider1) @@ -519,9 +526,9 @@ await repository.Shutdown(afterError: (provider, exception) => Assert.Equal(1, callCountShutdown1); Assert.Equal(1, callCountShutdown2); - provider1.Received(1).Shutdown(); - provider2.Received(1).Shutdown(); - provider3.Received(1).Shutdown(); + provider1.Received(1).ShutdownAsync(); + provider2.Received(1).ShutdownAsync(); + provider3.Received(1).ShutdownAsync(); } [Fact] @@ -531,12 +538,12 @@ public async Task Setting_Same_Default_Provider_Has_No_Effect() var provider = Substitute.For(); provider.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider, context); - await repository.SetProvider(provider, context); + await repository.SetProviderAsync(provider, context); + await repository.SetProviderAsync(provider, context); Assert.Equal(provider, repository.GetProvider()); - provider.Received(1).Initialize(context); - provider.DidNotReceive().Shutdown(); + provider.Received(1).InitializeAsync(context); + provider.DidNotReceive().ShutdownAsync(); } [Fact] @@ -546,12 +553,12 @@ public async Task Setting_Null_Default_Provider_Has_No_Effect() var provider = Substitute.For(); provider.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider, context); - await repository.SetProvider(null, context); + await repository.SetProviderAsync(provider, context); + await repository.SetProviderAsync(null, context); Assert.Equal(provider, repository.GetProvider()); - provider.Received(1).Initialize(context); - provider.DidNotReceive().Shutdown(); + provider.Received(1).InitializeAsync(context); + provider.DidNotReceive().ShutdownAsync(); } [Fact] @@ -566,10 +573,10 @@ public async Task Setting_Null_Named_Provider_Removes_It() defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(defaultProvider, context); + await repository.SetProviderAsync(defaultProvider, context); - await repository.SetProvider("named-provider", namedProvider, context); - await repository.SetProvider("named-provider", null, context); + await repository.SetProviderAsync("named-provider", namedProvider, context); + await repository.SetProviderAsync("named-provider", null, context); Assert.Equal(defaultProvider, repository.GetProvider("named-provider")); } @@ -582,15 +589,15 @@ public async Task Setting_Named_Provider_With_Null_Name_Has_No_Effect() var defaultProvider = Substitute.For(); defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); - await repository.SetProvider(defaultProvider, context); + await repository.SetProviderAsync(defaultProvider, context); var namedProvider = Substitute.For(); namedProvider.GetStatus().Returns(ProviderStatus.NotReady); - await repository.SetProvider(null, namedProvider, context); + await repository.SetProviderAsync(null, namedProvider, context); - namedProvider.DidNotReceive().Initialize(context); - namedProvider.DidNotReceive().Shutdown(); + namedProvider.DidNotReceive().InitializeAsync(context); + namedProvider.DidNotReceive().ShutdownAsync(); Assert.Equal(defaultProvider, repository.GetProvider(null)); } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index cdb59a0c..26fc0231 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; @@ -11,25 +12,25 @@ public class TestHookNoOverride : Hook { } public class TestHook : Hook { - public override Task Before(HookContext context, IReadOnlyDictionary? hints = null) + public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.FromResult(EvaluationContext.Empty); + return new ValueTask(EvaluationContext.Empty); } - public override Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null) + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } - public override Task Error(HookContext context, Exception error, IReadOnlyDictionary? hints = null) + public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } - public override Task Finally(HookContext context, IReadOnlyDictionary? hints = null) + public override ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } } @@ -64,32 +65,32 @@ public override Metadata GetMetadata() return new Metadata(this.Name); } - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext? context = null) + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); } - public override Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext? context = null) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext? context = null) + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } @@ -104,16 +105,16 @@ public void SetStatus(ProviderStatus status) this._status = status; } - public override Task Initialize(EvaluationContext context) + public override async ValueTask InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { this._status = ProviderStatus.Ready; - this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }); - return base.Initialize(context); + await this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }, cancellationToken).ConfigureAwait(false); + await base.InitializeAsync(context, cancellationToken).ConfigureAwait(false); } - internal void SendEvent(ProviderEventTypes eventType) + internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) { - this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name }); + return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name }, cancellationToken); } } } From f49ce15d7dba6f9755b39db378a7c4768b3ad739 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 1 May 2024 13:14:25 -0400 Subject: [PATCH 2/9] fixup: resolve conflicts, fix tests Signed-off-by: Todd Baert --- README.md | 415 ++---------------- src/OpenFeature/Api.cs | 56 +-- src/OpenFeature/EventExecutor.cs | 45 +- src/OpenFeature/FeatureProvider.cs | 12 +- src/OpenFeature/Model/EvaluationContext.cs | 10 + src/OpenFeature/OpenFeatureClient.cs | 8 +- src/OpenFeature/ProviderRepository.cs | 4 +- .../Providers/Memory/InMemoryProvider.cs | 28 +- .../Steps/EvaluationStepDefinitions.cs | 7 +- .../OpenFeatureClientTests.cs | 8 - .../OpenFeatureEventTests.cs | 2 +- test/OpenFeature.Tests/OpenFeatureTests.cs | 66 ++- .../ProviderRepositoryTests.cs | 29 +- .../Providers/Memory/InMemoryProviderTests.cs | 26 +- test/OpenFeature.Tests/TestImplementations.cs | 2 +- 15 files changed, 139 insertions(+), 579 deletions(-) diff --git a/README.md b/README.md index 27c2f668..7a620bcc 100644 --- a/README.md +++ b/README.md @@ -1,366 +1,24 @@ -<<<<<<< HEAD - - - -![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) - -## .NET SDK - - - -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) -[ - ![Release](https://img.shields.io/static/v1?label=release&message=v1.5.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.5.0) - -[![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -[![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) -[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) - - -[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. - - - -## 🚀 Quick start - -### Requirements - -- .NET 6+ -- .NET Core 6+ -- .NET Framework 4.6.2+ - -Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 - -### Install - -Use the following to initialize your project: - -```sh -dotnet new console -``` - -and install OpenFeature: - -```sh -dotnet add package OpenFeature -``` - -### Usage - -```csharp -public async Task Example() -{ - // Register your feature flag provider - await Api.Instance.SetProvider(new InMemoryProvider()); - - // Create a new client - FeatureClient client = Api.Instance.GetClient(); - - // Evaluate your feature flag - bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); - - if ( v2Enabled ) - { - //Do some work - } -} -``` - -## 🌟 Features - -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| ✅ | [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. | -| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | - -> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ - -### Providers - -[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. -Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). - -If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. - -Once you've added a provider as a dependency, it can be registered with OpenFeature like this: - -```csharp -await Api.Instance.SetProvider(new MyProvider()); -``` - -In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more detail below. - -### Targeting - -Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. -In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). -If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). - -```csharp -// set a value to the global context -EvaluationContextBuilder builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext apiCtx = builder.Build(); -Api.Instance.SetContext(apiCtx); - -// set a value to the client context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext clientCtx = builder.Build(); -var client = Api.Instance.GetClient(); -client.SetContext(clientCtx); - -// set a value to the invocation context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext reqCtx = builder.Build(); - -bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); - -``` - -### Hooks - -[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. -If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. - -Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. - -```csharp -// add a hook globally, to run on all evaluations -Api.Instance.AddHooks(new ExampleGlobalHook()); - -// add a hook on this client, to run on all evaluations made by this client -var client = Api.Instance.GetClient(); -client.AddHooks(new ExampleClientHook()); - -// add a hook for this evaluation only -var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); -``` - -### 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. - -### Named clients - -Clients can be given a name. -A name is a logical identifier that can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. - -```csharp -// registering the default provider -await Api.Instance.SetProvider(new LocalProvider()); - -// registering a named provider -await Api.Instance.SetProvider("clientForCache", new CachedProvider()); - -// a client backed by default provider -FeatureClient clientDefault = Api.Instance.GetClient(); - -// a client backed by CachedProvider -FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); - -``` - -### 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); -``` - -### Shutdown - -The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. - -```csharp -// Shut down all providers -await Api.Instance.Shutdown(); -``` - -## Extending - -### Develop a provider - -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. - -```csharp -public class MyProvider : FeatureProvider -{ - public override Metadata GetMetadata() - { - return new Metadata("My Provider"); - } - - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) - { - // resolve a boolean flag value - } - - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) - { - // resolve a double flag value - } - - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) - { - // resolve an int flag value - } - - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) - { - // resolve a string flag value - } - - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) - { - // resolve an object flag value - } -} -``` - -### Develop a hook - -To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -Implement your own hook by conforming to the `Hook interface`. -To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. - -```csharp -public class MyHook : Hook -{ - public Task Before(HookContext context, - IReadOnlyDictionary hints = null) - { - // code to run before flag evaluation - } - - public virtual Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) - { - // code to run after successful flag evaluation - } - - public virtual Task Error(HookContext context, Exception error, - IReadOnlyDictionary hints = null) - { - // code to run if there's an error during before hooks or during flag evaluation - } - - public virtual Task Finally(HookContext context, IReadOnlyDictionary hints = null) - { - // code to run after all other stages, regardless of success/failure - } -} -``` - -Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! - - -## ⭐️ Support the project - -- Give this repo a ⭐️! -- Follow us on social media: - - Twitter: [@openfeature](https://twitter.com/openfeature) - - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) -- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more information, check out our [community page](https://openfeature.dev/community/) - -## 🤝 Contributing - -Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. - -### Thanks to everyone who has already contributed - -[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) - -Made with [contrib.rocks](https://contrib.rocks). - -======= -

- - - OpenFeature Logo - -

+ +![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) -

OpenFeature .NET SDK

+## .NET SDK - -

- - Specification - - - - - Release - - -
- - Slack - - - Codecov - - - NuGet - - - CII Best Practices - - -

+ +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) +[ + ![Release](https://img.shields.io/static/v1?label=release&message=v1.5.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.5.0) + +[![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) +[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) -[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. @@ -394,7 +52,7 @@ dotnet add package OpenFeature public async Task Example() { // Register your feature flag provider - await Api.Instance.SetProviderAsync(new InMemoryProvider()); + await Api.Instance.SetProvider(new InMemoryProvider()); // Create a new client FeatureClient client = Api.Instance.GetClient(); @@ -422,7 +80,7 @@ public async Task Example() | ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | -Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ +> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ ### Providers @@ -465,14 +123,14 @@ builder = EvaluationContext.Builder(); builder.Set("region", "us-east-1"); EvaluationContext reqCtx = builder.Build(); -bool flagValue = await client.GetBooleanValueAsync("some-flag", false, reqCtx); +bool flagValue = await client.GetBooleanValuAsync("some-flag", false, reqCtx); ``` ### Hooks [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/). +Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. @@ -501,10 +159,10 @@ If a name has no associated provider, the global provider is used. ```csharp // registering the default provider -await Api.Instance.SetProvider(new LocalProvider()); +await Api.Instance.SetProviderAsync(new LocalProvider()); // registering a named provider -await Api.Instance.SetProvider("clientForCache", new CachedProvider()); +await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); // a client backed by default provider FeatureClient clientDefault = Api.Instance.GetClient(); @@ -576,27 +234,27 @@ public class MyProvider : FeatureProvider return new Metadata("My Provider"); } - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { // resolve a boolean flag value } - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - // resolve a double flag value + // resolve a string flag value } - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) { // resolve an int flag value } - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - // resolve a string flag value + // resolve a double flag value } - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { // resolve an object flag value } @@ -608,30 +266,30 @@ public class MyProvider : FeatureProvider To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. Implement your own hook by conforming to the `Hook interface`. -To satisfy the interface, all methods (`BeforeAsync`/`AfterAsync`/`FinallyAsync`/`ErrorAsync`) need to be defined. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. ```csharp public class MyHook : Hook { public ValueTask BeforeAsync(HookContext context, - IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + IReadOnlyDictionary hints = null) { // code to run before flag evaluation } - public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null) { // code to run after successful flag evaluation } - public virtual ValueTask ErrorAsync(HookContext context, Exception error, - IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + public ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary hints = null) { // code to run if there's an error during before hooks or during flag evaluation } - public virtual ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + public ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null) { // code to run after all other stages, regardless of success/failure } @@ -656,10 +314,7 @@ Interested in contributing? Great, we'd love your help! To get started, take a l ### Thanks to everyone who has already contributed - - - +[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) Made with [contrib.rocks](https://contrib.rocks). ->>>>>>> f23274b (refactor: Cleanup + plumb cancellation tokens) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 564d838f..4ff5579f 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -37,41 +37,18 @@ static Api() { } private Api() { } /// - /// Sets the default feature provider to given clientName without awaiting its initialization. - /// - /// The provider cannot be set to null. Attempting to set the provider to null has no effect. - /// Implementation of - [Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")] - public void SetProvider(FeatureProvider featureProvider) - { - this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - _ = this._repository.SetProvider(featureProvider, this.GetContext()); - } - - /// - /// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete, + /// Sets the feature provider. In order to wait for the provider to be set, and initialization to complete, /// await the returned task. /// /// The provider cannot be set to null. Attempting to set the provider to null has no effect. /// Implementation of - /// The . - public async ValueTask SetProviderAsync(FeatureProvider featureProvider, CancellationToken cancellationToken = default) + /// The to cancel any async side effects. + public async Task SetProviderAsync(FeatureProvider featureProvider, CancellationToken cancellationToken = default) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProviderAsync(featureProvider, this.GetContext(), cancellationToken: cancellationToken).ConfigureAwait(false); } - /// - /// Sets the feature provider to given clientName without awaiting its initialization. - /// - /// Name of client - /// Implementation of - [Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")] - public void SetProvider(string clientName, FeatureProvider featureProvider) - { - this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - _ = this._repository.SetProvider(clientName, featureProvider, this.GetContext()); - } /// /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and @@ -79,8 +56,8 @@ public void SetProvider(string clientName, FeatureProvider featureProvider) /// /// Name of client /// Implementation of - /// The . - public async ValueTask SetProviderAsync(string clientName, FeatureProvider featureProvider, CancellationToken cancellationToken = default) + /// The to cancel any async side effects. + public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider, CancellationToken cancellationToken = default) { this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), cancellationToken: cancellationToken).ConfigureAwait(false); @@ -246,22 +223,17 @@ public EvaluationContext GetContext() /// Once shut down is complete, API is reset and ready to use again. /// ///
- /// The . - public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) + /// The to cancel any async side effects. + public async Task ShutdownAsync(CancellationToken cancellationToken = default) { - // TODO: conflict - // await using (this._eventExecutor.ConfigureAwait(false)) - // await using (this._repository.ConfigureAwait(false)) - // { - // this._evaluationContext = EvaluationContext.Empty; - // this._hooks.Clear(); + await this._repository.ShutdownAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + await this._eventExecutor.ShutdownAsync(cancellationToken).ConfigureAwait(false); + this._evaluationContext = EvaluationContext.Empty; + this._hooks.Clear(); - // // TODO: make these lazy to avoid extra allocations on the common cleanup path? - // this._eventExecutor = new EventExecutor(); - // this._repository = new ProviderRepository(); - // } - // await this._repository.ShutdownAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - // await this.EventExecutor.ShutdownAsync(cancellationToken).ConfigureAwait(false); + // TODO: make these lazy to avoid extra allocations on the common cleanup path? + this._eventExecutor = new EventExecutor(); + this._repository = new ProviderRepository(); } /// diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 1fd6d96f..47c30f66 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -10,9 +10,9 @@ namespace OpenFeature { - internal delegate ValueTask ShutdownDelegate(CancellationToken cancellationToken); + internal delegate Task ShutdownDelegate(CancellationToken cancellationToken); - internal sealed partial class EventExecutor : IAsyncDisposable + internal sealed partial class EventExecutor { private readonly object _lockObj = new object(); public readonly Channel EventChannel = Channel.CreateBounded(1); @@ -32,8 +32,6 @@ public EventExecutor() eventProcessing.Start(); } - public ValueTask DisposeAsync() => new(this.Shutdown()); - internal void SetLogger(ILogger logger) => this._logger = logger; internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) @@ -319,43 +317,14 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } } - [LoggerMessage(100, LogLevel.Error, "Error running handler")] - partial void ErrorRunningHandler(Exception exception); - - public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) + public async Task ShutdownAsync(CancellationToken cancellationToken = default) { - await this._shutdownDelegate(cancellationToken).ConfigureAwait(false); + this.EventChannel.Writer.Complete(); + await this.EventChannel.Reader.Completion.ConfigureAwait(false); } - internal void SetShutdownDelegate(ShutdownDelegate del) - { - this._shutdownDelegate = del; - } - - // Method to signal shutdown - private async ValueTask SignalShutdownAsync(CancellationToken cancellationToken) - { - // Enqueue a shutdown signal - await this.EventChannel.Writer.WriteAsync(new ShutdownSignal(), cancellationToken).ConfigureAwait(false); - - // Wait for the processing loop to acknowledge the shutdown - await this._shutdownSemaphore.WaitAsync(cancellationToken).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; - } + [LoggerMessage(100, LogLevel.Error, "Error running handler")] + partial void ErrorRunningHandler(Exception exception); } internal class Event diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index b7abe6f6..62976f53 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -113,7 +113,7 @@ public abstract Task> ResolveStructureValueAsync(string /// /// /// - /// The . + /// The to cancel any async side effects. /// A task that completes when the initialization process is complete. /// /// @@ -125,10 +125,10 @@ public abstract Task> ResolveStructureValueAsync(string /// the method after initialization is complete. /// /// - public virtual ValueTask InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) + public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { // Intentionally left blank. - return new ValueTask(); + return Task.CompletedTask; } /// @@ -136,11 +136,11 @@ public virtual ValueTask InitializeAsync(EvaluationContext context, Cancellation /// Providers can overwrite this method, if they have special shutdown actions needed. /// /// A task that completes when the shutdown process is complete. - /// The . - public virtual ValueTask ShutdownAsync(CancellationToken cancellationToken = default) + /// The to cancel any async side effects. + public virtual Task ShutdownAsync(CancellationToken cancellationToken = default) { // Intentionally left blank. - return new ValueTask(); + return Task.CompletedTask; } /// diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs index 59b1fe20..f377b9db 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -24,6 +24,16 @@ internal EvaluationContext(string? targetingKey, Structure content) this._structure = content; } + /// + /// Internal constructor used by the builder. + /// + /// The content of the context. + internal EvaluationContext(Structure content) + { + this.TargetingKey = string.Empty; + this._structure = content; + } + /// /// Private constructor for making an empty . /// diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 9e32d78a..674b78a7 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -274,7 +274,7 @@ private async Task> EvaluateFlagAsync( return evaluation; } - private async ValueTask> TriggerBeforeHooksAsync(IReadOnlyList hooks, HookContext context, + private async Task> TriggerBeforeHooksAsync(IReadOnlyList hooks, HookContext context, FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { var evalContextBuilder = EvaluationContext.Builder(); @@ -297,7 +297,7 @@ private async ValueTask> TriggerBeforeHooksAsync(IReadOnlyList return context.WithNewEvaluationContext(evalContextBuilder.Build()); } - private async ValueTask TriggerAfterHooksAsync(IReadOnlyList hooks, HookContext context, + private async Task TriggerAfterHooksAsync(IReadOnlyList hooks, HookContext context, FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) @@ -306,7 +306,7 @@ private async ValueTask TriggerAfterHooksAsync(IReadOnlyList hooks, Hoo } } - private async ValueTask TriggerErrorHooksAsync(IReadOnlyList hooks, HookContext context, Exception exception, + private async Task TriggerErrorHooksAsync(IReadOnlyList hooks, HookContext context, Exception exception, FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) @@ -322,7 +322,7 @@ private async ValueTask TriggerErrorHooksAsync(IReadOnlyList hooks, Hoo } } - private async ValueTask TriggerFinallyHooksAsync(IReadOnlyList hooks, HookContext context, + private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, HookContext context, FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 8e756b25..a21f8679 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -62,7 +62,7 @@ public async ValueTask DisposeAsync() /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - /// The . + /// The to cancel any async side effects. public async ValueTask SetProviderAsync( FeatureProvider? featureProvider, EvaluationContext context, @@ -155,7 +155,7 @@ private static async ValueTask InitProviderAsync( /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - /// The . + /// The to cancel any async side effects. public async ValueTask SetProviderAsync(string clientName, FeatureProvider? featureProvider, EvaluationContext context, diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 766e4f3c..e56acdb5 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Error; @@ -45,7 +46,7 @@ public InMemoryProvider(IDictionary? flags = null) /// Updating provider flags configuration, replacing all flags. /// /// the flags to use instead of the previous flags. - public async ValueTask UpdateFlags(IDictionary? flags = null) + public async Task UpdateFlags(IDictionary? flags = null) { var changed = this._flags.Keys.ToList(); if (flags == null) @@ -68,46 +69,31 @@ public async ValueTask UpdateFlags(IDictionary? flags = null) } /// - public override Task> ResolveBooleanValue( - string flagKey, - bool defaultValue, - EvaluationContext? context = null) + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveStringValue( - string flagKey, - string defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveIntegerValue( - string flagKey, - int defaultValue, - EvaluationContext? context = null) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveDoubleValue( - string flagKey, - double defaultValue, - EvaluationContext? context = null) + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveStructureValue( - string flagKey, - Value defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index c066409e..f2690d6b 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Extension; @@ -39,10 +40,10 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) } [Given(@"a provider is registered")] - public void GivenAProviderIsRegistered() + public async void GivenAProviderIsRegistered() { var memProvider = new InMemoryProvider(e2eFlagConfig); - Api.Instance.SetProviderAsync(memProvider).Wait(); + await Api.Instance.SetProviderAsync(memProvider).ConfigureAwait(false); client = Api.Instance.GetClient("TestClient", "1.0.0"); } @@ -218,7 +219,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey, contextAwareDefaultValue, new EvaluationContext(new Structure(ImmutableDictionary.Empty))).Result; + string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey!, contextAwareDefaultValue!, new EvaluationContext(new Structure(ImmutableDictionary.Empty))).Result; Assert.Equal(expected, emptyContextValue); } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index ad90bb51..b36947d4 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -344,14 +344,6 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } - [Fact] - public async Task Should_Use_No_Op_When_Provider_Is_Null() - { - await Api.Instance.SetProviderAsync(null); - var client = new FeatureClient("test", "test"); - (await client.GetIntegerValueAsync("some-key", 12)).Should().Be(12); - } - [Fact] public void Should_Get_And_Set_Context() { diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 2502bd8c..384928d6 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -148,7 +148,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ var testProvider = new TestProvider(); #pragma warning disable CS0618// Type or member is obsolete - Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); #pragma warning restore CS0618// Type or member is obsolete Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 99c4db61..673c183d 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,7 +1,5 @@ -using System; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading; using System.Threading.Tasks; using FluentAssertions; using NSubstitute; @@ -15,8 +13,6 @@ namespace OpenFeature.Tests [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { - static ValueTask EmptyShutdown(CancellationToken cancellationToken) => new ValueTask(); - [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() @@ -34,14 +30,14 @@ public async Task OpenFeature_Should_Initialize_Provider() var providerMockDefault = Substitute.For(); providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerMockDefault).ConfigureAwait(false); - await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerMockDefault); + await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerMockNamed = Substitute.For(); providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("the-name", providerMockNamed).ConfigureAwait(false); - await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("the-name", providerMockNamed); + await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()); } [Fact] @@ -52,28 +48,28 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() var providerA = Substitute.For(); providerA.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); - await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerA); + await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerB).ConfigureAwait(false); - await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); - await providerA.Received(1).ShutdownAsync().ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerB); + await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()); + await providerA.Received(1).ShutdownAsync(); var providerC = Substitute.For(); providerC.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("named", providerC).ConfigureAwait(false); - await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerC); + await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerD = Substitute.For(); providerD.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("named", providerD).ConfigureAwait(false); - await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()).ConfigureAwait(false); - await providerC.Received(1).ShutdownAsync().ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerD); + await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()); + await providerC.Received(1).ShutdownAsync(); } [Fact] @@ -86,13 +82,13 @@ public async Task OpenFeature_Should_Support_Shutdown() var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); - await Api.Instance.SetProviderAsync("named", providerB).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerA); + await Api.Instance.SetProviderAsync("named", providerB); - await Api.Instance.ShutdownAsync().ConfigureAwait(false); + await Api.Instance.ShutdownAsync(); - await providerA.Received(1).ShutdownAsync().ConfigureAwait(false); - await providerB.Received(1).ShutdownAsync().ConfigureAwait(false); + await providerA.Received(1).ShutdownAsync(); + await providerB.Received(1).ShutdownAsync(); } [Fact] @@ -101,8 +97,8 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def { var openFeature = Api.Instance; - await openFeature.SetProviderAsync(new NoOpFeatureProvider()).ConfigureAwait(false); - await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(new NoOpFeatureProvider()); + await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()); var defaultClient = openFeature.GetProviderMetadata(); var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); @@ -117,7 +113,7 @@ public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() { var openFeature = Api.Instance; - await openFeature.SetProviderAsync(new TestProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(new TestProvider()); var defaultClient = openFeature.GetProviderMetadata(); @@ -131,8 +127,8 @@ public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() const string name = "new-client"; var openFeature = Api.Instance; - await openFeature.SetProviderAsync(name, new TestProvider()).ConfigureAwait(false); - await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(name, new TestProvider()); + await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); } @@ -144,8 +140,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instanc var openFeature = Api.Instance; var provider = new TestProvider(); - await openFeature.SetProviderAsync("a", provider).ConfigureAwait(false); - await openFeature.SetProviderAsync("b", provider).ConfigureAwait(false); + await openFeature.SetProviderAsync("a", provider); + await openFeature.SetProviderAsync("b", provider); var clientA = openFeature.GetProvider("a"); var clientB = openFeature.GetProvider("b"); @@ -186,7 +182,7 @@ public void OpenFeature_Should_Add_Hooks() [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] public async Task OpenFeature_Should_Get_Metadata() { - Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).GetAwaiter().GetResult(); + await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); var openFeature = Api.Instance; var metadata = openFeature.GetProviderMetadata(); @@ -236,8 +232,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() { var openFeature = Api.Instance; - await openFeature.SetProviderAsync("client1", new TestProvider()).ConfigureAwait(false); - await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync("client1", new TestProvider()); + await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()); var client1 = openFeature.GetClient("client1"); var client2 = openFeature.GetClient("client2"); @@ -245,8 +241,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() client1.GetMetadata().Name.Should().Be("client1"); client2.GetMetadata().Name.Should().Be("client2"); - client1.GetBooleanValueAsync("test", false).Result.Should().BeTrue(); - client2.GetBooleanValueAsync("test", false).Result.Should().BeFalse(); + (await client1.GetBooleanValueAsync("test", false)).Should().BeTrue(); + (await client2.GetBooleanValueAsync("test", false)).Should().BeFalse(); } } } diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 4178e595..3408f3b4 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -22,7 +22,7 @@ public async Task Default_Provider_Is_Set_Without_Await() var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); // TODO: huh?? await?? - repository.SetProviderAsync(provider, context); + await repository.SetProviderAsync(provider, context); Assert.Equal(provider, repository.GetProvider()); } @@ -35,7 +35,7 @@ public async void AfterSet_Is_Invoked_For_Setting_Default_Provider() var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. // TODO: huh?? await?? - repository.SetProviderAsync(provider, context, afterSet: (theProvider) => + await repository.SetProviderAsync(provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -191,7 +191,7 @@ public async Task Named_Provider_Provider_Is_Set_Without_Await() var context = new EvaluationContextBuilder().Build(); // TODO: huh?? await?? - repository.SetProviderAsync("the-name", provider, context); + await repository.SetProviderAsync("the-name", provider, context); Assert.Equal(provider, repository.GetProvider("the-name")); } @@ -204,7 +204,7 @@ public async Task AfterSet_Is_Invoked_For_Setting_Named_Provider() var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. // TODO: huh?? await?? - repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => + await repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -580,26 +580,5 @@ public async Task Setting_Null_Named_Provider_Removes_It() Assert.Equal(defaultProvider, repository.GetProvider("named-provider")); } - - [Fact] - public async Task Setting_Named_Provider_With_Null_Name_Has_No_Effect() - { - var repository = new ProviderRepository(); - var context = new EvaluationContextBuilder().Build(); - - var defaultProvider = Substitute.For(); - defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync(defaultProvider, context); - - var namedProvider = Substitute.For(); - namedProvider.GetStatus().Returns(ProviderStatus.NotReady); - - await repository.SetProviderAsync(null, namedProvider, context); - - namedProvider.DidNotReceive().InitializeAsync(context); - namedProvider.DidNotReceive().ShutdownAsync(); - - Assert.Equal(defaultProvider, repository.GetProvider(null)); - } } } diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 64e1df46..13bd8dfb 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -111,7 +111,7 @@ public InMemoryProviderTests() [Fact] public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveBooleanValueAsync("boolean-flag", false, EvaluationContext.Empty); Assert.True(details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("on", details.Variant); @@ -120,7 +120,7 @@ public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetString_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("string-flag", "nope", EvaluationContext.Empty); Assert.Equal("hi", details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("greeting", details.Variant); @@ -129,7 +129,7 @@ public async void GetString_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetInt_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveIntegerValueAsync("integer-flag", 13, EvaluationContext.Empty); Assert.Equal(10, details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("ten", details.Variant); @@ -138,7 +138,7 @@ public async void GetInt_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetDouble_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveDoubleValueAsync("float-flag", 13, EvaluationContext.Empty); Assert.Equal(0.5, details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("half", details.Variant); @@ -147,7 +147,7 @@ public async void GetDouble_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetStruct_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveStructureValueAsync("object-flag", new Value(), EvaluationContext.Empty); Assert.Equal(true, details.Value.AsStructure?["showImages"].AsBoolean); Assert.Equal("Check out these pics!", details.Value.AsStructure?["title"].AsString); Assert.Equal(100, details.Value.AsStructure?["imagesPerPage"].AsInteger); @@ -159,7 +159,7 @@ public async void GetStruct_ShouldEvaluateWithReasonAndVariant() public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() { EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); - ResolutionDetails details = await this.commonProvider.ResolveStringValue("context-aware", "nope", context); + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("context-aware", "nope", context); Assert.Equal("INTERNAL", details.Value); Assert.Equal(Reason.TargetingMatch, details.Reason); Assert.Equal("internal", details.Variant); @@ -176,25 +176,25 @@ public async void EmptyFlags_ShouldWork() [Fact] public async void MissingFlag_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("missing-flag", false, EvaluationContext.Empty)); } [Fact] public async void MismatchedFlag_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveStringValueAsync("boolean-flag", "nope", EvaluationContext.Empty)); } [Fact] public async void MissingDefaultVariant_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-flag", false, EvaluationContext.Empty)); } [Fact] public async void MissingEvaluatedVariant_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-evaluator-flag", false, EvaluationContext.Empty)); } [Fact] @@ -211,7 +211,7 @@ public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() ) }}); - ResolutionDetails details = await provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty); + ResolutionDetails details = await provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty); Assert.True(details.Value); // update flags @@ -229,10 +229,10 @@ await provider.UpdateFlags(new Dictionary(){ var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res?.Type); - await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty)); // new flag should be present, old gone (defaults), handler run. - ResolutionDetails detailsAfter = await provider.ResolveStringValue("new-flag", "nope", EvaluationContext.Empty); + ResolutionDetails detailsAfter = await provider.ResolveStringValueAsync("new-flag", "nope", EvaluationContext.Empty); Assert.True(details.Value); Assert.Equal("hi", detailsAfter.Value); } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index 26fc0231..c949b373 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -105,7 +105,7 @@ public void SetStatus(ProviderStatus status) this._status = status; } - public override async ValueTask InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) + public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { this._status = ProviderStatus.Ready; await this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }, cancellationToken).ConfigureAwait(false); From 0f9219888714463e7dcbed480e789ff83b4f133a Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 1 May 2024 15:08:35 -0400 Subject: [PATCH 3/9] fixup: remove TODOs Signed-off-by: Todd Baert --- test/OpenFeature.Tests/ProviderRepositoryTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 3408f3b4..2fbfb274 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -21,7 +21,6 @@ public async Task Default_Provider_Is_Set_Without_Await() var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - // TODO: huh?? await?? await repository.SetProviderAsync(provider, context); Assert.Equal(provider, repository.GetProvider()); } @@ -34,7 +33,6 @@ public async void AfterSet_Is_Invoked_For_Setting_Default_Provider() var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - // TODO: huh?? await?? await repository.SetProviderAsync(provider, context, afterSet: (theProvider) => { callCount++; @@ -189,7 +187,6 @@ public async Task Named_Provider_Provider_Is_Set_Without_Await() var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - // TODO: huh?? await?? await repository.SetProviderAsync("the-name", provider, context); Assert.Equal(provider, repository.GetProvider("the-name")); @@ -203,7 +200,6 @@ public async Task AfterSet_Is_Invoked_For_Setting_Named_Provider() var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - // TODO: huh?? await?? await repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => { callCount++; @@ -249,7 +245,6 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider( var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; - // TODO: huh?? await?? Exception? receivedError = null; await repository.SetProviderAsync("the-provider", providerMock, context, afterError: (theProvider, error) => { @@ -343,7 +338,6 @@ public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync("the-name", provider1, context); var callCount = 0; - // TODO: huh?? await?? Exception? errorThrown = null; await repository.SetProviderAsync("the-name", provider2, context, afterError: (provider, ex) => { From 950a0a5858c1aca050cde741a66ee51e19a087bf Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 2 May 2024 09:08:20 -0400 Subject: [PATCH 4/9] fixup: review feedback Signed-off-by: Todd Baert --- README.md | 2 +- src/OpenFeature/Api.cs | 4 ++++ src/OpenFeature/Model/EvaluationContext.cs | 10 ---------- .../Steps/EvaluationStepDefinitions.cs | 3 +-- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7a620bcc..6844915f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ dotnet add package OpenFeature public async Task Example() { // Register your feature flag provider - await Api.Instance.SetProvider(new InMemoryProvider()); + await Api.Instance.SetProviderAsync(new InMemoryProvider()); // Create a new client FeatureClient client = Api.Instance.GetClient(); diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 4ff5579f..d80590a2 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -59,6 +59,10 @@ public async Task SetProviderAsync(FeatureProvider featureProvider, Cancellation /// The to cancel any async side effects. public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider, CancellationToken cancellationToken = default) { + if (string.IsNullOrWhiteSpace(clientName)) + { + throw new ArgumentNullException(nameof(clientName)); + } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), cancellationToken: cancellationToken).ConfigureAwait(false); } diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs index f377b9db..59b1fe20 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -24,16 +24,6 @@ internal EvaluationContext(string? targetingKey, Structure content) this._structure = content; } - /// - /// Internal constructor used by the builder. - /// - /// The content of the context. - internal EvaluationContext(Structure content) - { - this.TargetingKey = string.Empty; - this._structure = content; - } - /// /// Private constructor for making an empty . /// diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index f2690d6b..d8fbb336 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Collections.Immutable; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Extension; @@ -219,7 +218,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey!, contextAwareDefaultValue!, new EvaluationContext(new Structure(ImmutableDictionary.Empty))).Result; + string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey!, contextAwareDefaultValue!, EvaluationContext.Empty).Result; Assert.Equal(expected, emptyContextValue); } From 8bbf69d0d529d3a75f1566985f7d81f40aff8c69 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 3 May 2024 14:23:37 -0400 Subject: [PATCH 5/9] fixup: remove ct for init/shutdown Signed-off-by: Todd Baert --- src/OpenFeature/Api.cs | 29 +++++++++---------- src/OpenFeature/EventExecutor.cs | 6 ++-- src/OpenFeature/ProviderRepository.cs | 41 ++++++++++++--------------- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index d80590a2..6f13cac2 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -42,11 +42,10 @@ private Api() { } /// /// The provider cannot be set to null. Attempting to set the provider to null has no effect. /// Implementation of - /// The to cancel any async side effects. - public async Task SetProviderAsync(FeatureProvider featureProvider, CancellationToken cancellationToken = default) + public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProviderAsync(featureProvider, this.GetContext(), cancellationToken: cancellationToken).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext()).ConfigureAwait(false); } @@ -56,15 +55,14 @@ public async Task SetProviderAsync(FeatureProvider featureProvider, Cancellation /// /// Name of client /// Implementation of - /// The to cancel any async side effects. - public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider, CancellationToken cancellationToken = default) + public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider) { if (string.IsNullOrWhiteSpace(clientName)) { throw new ArgumentNullException(nameof(clientName)); } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), cancellationToken: cancellationToken).ConfigureAwait(false); + await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); } /// @@ -227,17 +225,18 @@ public EvaluationContext GetContext() /// Once shut down is complete, API is reset and ready to use again. /// /// - /// The to cancel any async side effects. - public async Task ShutdownAsync(CancellationToken cancellationToken = default) + public async Task ShutdownAsync() { - await this._repository.ShutdownAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - await this._eventExecutor.ShutdownAsync(cancellationToken).ConfigureAwait(false); - this._evaluationContext = EvaluationContext.Empty; - this._hooks.Clear(); + await using (this._eventExecutor.ConfigureAwait(false)) + await using (this._repository.ConfigureAwait(false)) + { + this._evaluationContext = EvaluationContext.Empty; + this._hooks.Clear(); - // TODO: make these lazy to avoid extra allocations on the common cleanup path? - this._eventExecutor = new EventExecutor(); - this._repository = new ProviderRepository(); + // TODO: make these lazy to avoid extra allocations on the common cleanup path? + this._eventExecutor = new EventExecutor(); + this._repository = new ProviderRepository(); + } } /// diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 47c30f66..886a47b6 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -12,7 +12,7 @@ namespace OpenFeature { internal delegate Task ShutdownDelegate(CancellationToken cancellationToken); - internal sealed partial class EventExecutor + internal sealed partial class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); public readonly Channel EventChannel = Channel.CreateBounded(1); @@ -32,6 +32,8 @@ public EventExecutor() eventProcessing.Start(); } + public ValueTask DisposeAsync() => new(this.ShutdownAsync()); + internal void SetLogger(ILogger logger) => this._logger = logger; internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) @@ -317,7 +319,7 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } } - public async Task ShutdownAsync(CancellationToken cancellationToken = default) + public async Task ShutdownAsync() { this.EventChannel.Writer.Complete(); await this.EventChannel.Reader.Completion.ConfigureAwait(false); diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index a21f8679..7934da1c 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -62,15 +62,13 @@ public async ValueTask DisposeAsync() /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - /// The to cancel any async side effects. - public async ValueTask SetProviderAsync( + public async Task SetProviderAsync( FeatureProvider? featureProvider, EvaluationContext context, Action? afterSet = null, Action? afterInitialization = null, Action? afterError = null, - Action? afterShutdown = null, - CancellationToken cancellationToken = default) + Action? afterShutdown = null) { // Cannot unset the feature provider. if (featureProvider == null) @@ -94,7 +92,7 @@ public async ValueTask SetProviderAsync( // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. #pragma warning disable CS4014 - this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError, cancellationToken); + this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); #pragma warning restore CS4014 } finally @@ -102,16 +100,15 @@ public async ValueTask SetProviderAsync( this._providersLock.ExitWriteLock(); } - await InitProviderAsync(this._defaultProvider, context, afterInitialization, afterError, cancellationToken) + await InitProviderAsync(this._defaultProvider, context, afterInitialization, afterError) .ConfigureAwait(false); } - private static async ValueTask InitProviderAsync( + private static async Task InitProviderAsync( FeatureProvider? newProvider, EvaluationContext context, Action? afterInitialization, - Action? afterError, - CancellationToken cancellationToken) + Action? afterError) { if (newProvider == null) { @@ -121,7 +118,7 @@ private static async ValueTask InitProviderAsync( { try { - await newProvider.InitializeAsync(context, cancellationToken).ConfigureAwait(false); + await newProvider.InitializeAsync(context).ConfigureAwait(false); afterInitialization?.Invoke(newProvider); } catch (Exception ex) @@ -156,7 +153,7 @@ private static async ValueTask InitProviderAsync( /// /// called after a provider is shutdown, can be used to remove event handlers /// The to cancel any async side effects. - public async ValueTask SetProviderAsync(string clientName, + public async Task SetProviderAsync(string clientName, FeatureProvider? featureProvider, EvaluationContext context, Action? afterSet = null, @@ -192,7 +189,7 @@ public async ValueTask SetProviderAsync(string clientName, // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. #pragma warning disable CS4014 - this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError, cancellationToken); + this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); #pragma warning restore CS4014 } finally @@ -200,17 +197,16 @@ public async ValueTask SetProviderAsync(string clientName, this._providersLock.ExitWriteLock(); } - await InitProviderAsync(featureProvider, context, afterInitialization, afterError, cancellationToken).ConfigureAwait(false); + await InitProviderAsync(featureProvider, context, afterInitialization, afterError).ConfigureAwait(false); } /// /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. /// - private async ValueTask ShutdownIfUnusedAsync( + private async Task ShutdownIfUnusedAsync( FeatureProvider? targetProvider, Action? afterShutdown, - Action? afterError, - CancellationToken cancellationToken) + Action? afterError) { if (ReferenceEquals(this._defaultProvider, targetProvider)) { @@ -222,7 +218,7 @@ private async ValueTask ShutdownIfUnusedAsync( return; } - await SafeShutdownProviderAsync(targetProvider, afterShutdown, afterError, cancellationToken).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider, afterShutdown, afterError).ConfigureAwait(false); } /// @@ -234,10 +230,9 @@ private async ValueTask ShutdownIfUnusedAsync( /// it would not be meaningful to emit an error. /// /// - private static async ValueTask SafeShutdownProviderAsync(FeatureProvider? targetProvider, + private static async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider, Action? afterShutdown, - Action? afterError, - CancellationToken cancellationToken) + Action? afterError) { if (targetProvider == null) { @@ -246,7 +241,7 @@ private static async ValueTask SafeShutdownProviderAsync(FeatureProvider? target try { - await targetProvider.ShutdownAsync(cancellationToken).ConfigureAwait(false); + await targetProvider.ShutdownAsync().ConfigureAwait(false); afterShutdown?.Invoke(targetProvider); } catch (Exception ex) @@ -288,7 +283,7 @@ public FeatureProvider GetProvider(string? clientName) : this.GetProvider(); } - public async ValueTask ShutdownAsync(Action? afterError = null, CancellationToken cancellationToken = default) + public async Task ShutdownAsync(Action? afterError = null, CancellationToken cancellationToken = default) { var providers = new HashSet(); this._providersLock.EnterWriteLock(); @@ -312,7 +307,7 @@ public async ValueTask ShutdownAsync(Action? afterEr foreach (var targetProvider in providers) { // We don't need to take any actions after shutdown. - await SafeShutdownProviderAsync(targetProvider, null, afterError, cancellationToken).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider, null, afterError).ConfigureAwait(false); } } } From 036d7a45c5627c4fb17ce7ab7208cd52ca19b7ca Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 13 May 2024 14:46:18 -0400 Subject: [PATCH 6/9] fixup: add ct specific tests Signed-off-by: Todd Baert --- .../OpenFeatureClientTests.cs | 38 +++++++++++ .../OpenFeature.Tests/OpenFeatureHookTests.cs | 67 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index b36947d4..e7c76d75 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; @@ -344,6 +345,43 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } + [Fact] + public async Task Cancellation_Token_Added_Is_Passed_To_Provider() + { + var fixture = new Fixture(); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultString = fixture.Create(); + var cancelledReason = "cancelled"; + + var cts = new CancellationTokenSource(); + + + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => + { + var token = args.ArgAt(3); + while (!token.IsCancellationRequested) + { + await Task.Delay(10); // artificially delay until cancelled + } + return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); + }); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + + await Api.Instance.SetProviderAsync(clientName, featureProviderMock); + var client = Api.Instance.GetClient(clientName, clientVersion); + var task = client.GetStringDetailsAsync(flagName, defaultString, EvaluationContext.Empty, null, cts.Token); + cts.Cancel(); // cancel before awaiting + + var response = await task; + response.Value.Should().Be(defaultString); + response.Reason.Should().Be(cancelledReason); + _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultString, Arg.Any(), cts.Token); + } + [Fact] public void Should_Get_And_Set_Context() { diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 0e3effc1..9ca5b364 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -3,12 +3,14 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; using NSubstitute; using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; +using OpenFeature.Error; using OpenFeature.Model; using OpenFeature.Tests.Internal; using Xunit; @@ -554,6 +556,71 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); } + [Fact] + public async Task Successful_Resolution_Should_Pass_Cancellation_Token() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var cts = new CancellationTokenSource(); + + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + + hook.BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any(), cts.Token).Returns(new ResolutionDetails("test", false)); + _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + client.AddHooks(hook); + + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, null, cts.Token); + + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + } + + [Fact] + public async Task Failed_Resolution_Should_Pass_Cancellation_Token() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var flagOptions = new FlagEvaluationOptions(hook); + var exceptionToThrow = new GeneralException("Fake Exception"); + var cts = new CancellationTokenSource(); + + featureProvider.GetMetadata() + .Returns(new Metadata(null)); + + featureProvider.GetProviderHooks() + .Returns(ImmutableList.Empty); + + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty); + + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Throws(exceptionToThrow); + + hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) + .Returns(new ValueTask()); + + hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); + + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + + await client.GetBooleanValueAsync("test", true, EvaluationContext.Empty, flagOptions, cts.Token); + + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + + await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); + } + [Fact] public void Add_hooks_should_accept_empty_enumerable() { From 316356de571174c8bb2a291fc278cf147e7eaef2 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 14 May 2024 09:03:56 -0400 Subject: [PATCH 7/9] Update test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Todd Baert --- test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index d8fbb336..0cd6aacc 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -39,7 +39,7 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) } [Given(@"a provider is registered")] - public async void GivenAProviderIsRegistered() + public async Task GivenAProviderIsRegistered() { var memProvider = new InMemoryProvider(e2eFlagConfig); await Api.Instance.SetProviderAsync(memProvider).ConfigureAwait(false); From f9cafdfcf46766862b5f829356de841f304c5cef Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 14 May 2024 09:04:27 -0400 Subject: [PATCH 8/9] Update test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Todd Baert --- .../OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 13bd8dfb..a9eb3119 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -109,7 +109,7 @@ public InMemoryProviderTests() } [Fact] - public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() + public async Task GetBoolean_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveBooleanValueAsync("boolean-flag", false, EvaluationContext.Empty); Assert.True(details.Value); From dc3141d8befe2e221fe1eea6c6f70f911989092e Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 15 May 2024 15:41:08 -0400 Subject: [PATCH 9/9] fixup: review feedback Signed-off-by: Todd Baert --- .../Steps/EvaluationStepDefinitions.cs | 4 ++-- .../ProviderRepositoryTests.cs | 2 +- .../Providers/Memory/InMemoryProviderTests.cs | 23 ++++++++++--------- test/OpenFeature.Tests/TestUtilsTest.cs | 5 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 0cd6aacc..a50f3945 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -39,10 +39,10 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) } [Given(@"a provider is registered")] - public async Task GivenAProviderIsRegistered() + public void GivenAProviderIsRegistered() { var memProvider = new InMemoryProvider(e2eFlagConfig); - await Api.Instance.SetProviderAsync(memProvider).ConfigureAwait(false); + Api.Instance.SetProviderAsync(memProvider).Wait(); client = Api.Instance.GetClient("TestClient", "1.0.0"); } diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 2fbfb274..ccec89bd 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -26,7 +26,7 @@ public async Task Default_Provider_Is_Set_Without_Await() } [Fact] - public async void AfterSet_Is_Invoked_For_Setting_Default_Provider() + public async Task AfterSet_Is_Invoked_For_Setting_Default_Provider() { var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index a9eb3119..83974c23 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Error; using OpenFeature.Model; @@ -118,7 +119,7 @@ public async Task GetBoolean_ShouldEvaluateWithReasonAndVariant() } [Fact] - public async void GetString_ShouldEvaluateWithReasonAndVariant() + public async Task GetString_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("string-flag", "nope", EvaluationContext.Empty); Assert.Equal("hi", details.Value); @@ -127,7 +128,7 @@ public async void GetString_ShouldEvaluateWithReasonAndVariant() } [Fact] - public async void GetInt_ShouldEvaluateWithReasonAndVariant() + public async Task GetInt_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveIntegerValueAsync("integer-flag", 13, EvaluationContext.Empty); Assert.Equal(10, details.Value); @@ -136,7 +137,7 @@ public async void GetInt_ShouldEvaluateWithReasonAndVariant() } [Fact] - public async void GetDouble_ShouldEvaluateWithReasonAndVariant() + public async Task GetDouble_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveDoubleValueAsync("float-flag", 13, EvaluationContext.Empty); Assert.Equal(0.5, details.Value); @@ -145,7 +146,7 @@ public async void GetDouble_ShouldEvaluateWithReasonAndVariant() } [Fact] - public async void GetStruct_ShouldEvaluateWithReasonAndVariant() + public async Task GetStruct_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveStructureValueAsync("object-flag", new Value(), EvaluationContext.Empty); Assert.Equal(true, details.Value.AsStructure?["showImages"].AsBoolean); @@ -156,7 +157,7 @@ public async void GetStruct_ShouldEvaluateWithReasonAndVariant() } [Fact] - public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() + public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() { EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("context-aware", "nope", context); @@ -166,7 +167,7 @@ public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( } [Fact] - public async void EmptyFlags_ShouldWork() + public async Task EmptyFlags_ShouldWork() { var provider = new InMemoryProvider(); await provider.UpdateFlags(); @@ -174,31 +175,31 @@ public async void EmptyFlags_ShouldWork() } [Fact] - public async void MissingFlag_ShouldThrow() + public async Task MissingFlag_ShouldThrow() { await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("missing-flag", false, EvaluationContext.Empty)); } [Fact] - public async void MismatchedFlag_ShouldThrow() + public async Task MismatchedFlag_ShouldThrow() { await Assert.ThrowsAsync(() => this.commonProvider.ResolveStringValueAsync("boolean-flag", "nope", EvaluationContext.Empty)); } [Fact] - public async void MissingDefaultVariant_ShouldThrow() + public async Task MissingDefaultVariant_ShouldThrow() { await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-flag", false, EvaluationContext.Empty)); } [Fact] - public async void MissingEvaluatedVariant_ShouldThrow() + public async Task MissingEvaluatedVariant_ShouldThrow() { await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-evaluator-flag", false, EvaluationContext.Empty)); } [Fact] - public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() + public async Task PutConfiguration_shouldUpdateConfigAndRunHandlers() { var provider = new InMemoryProvider(new Dictionary(){ { diff --git a/test/OpenFeature.Tests/TestUtilsTest.cs b/test/OpenFeature.Tests/TestUtilsTest.cs index 1d0882b0..b65a91f5 100644 --- a/test/OpenFeature.Tests/TestUtilsTest.cs +++ b/test/OpenFeature.Tests/TestUtilsTest.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using Xunit; namespace OpenFeature.Tests @@ -8,13 +9,13 @@ namespace OpenFeature.Tests public class TestUtilsTest { [Fact] - public async void Should_Fail_If_Assertion_Fails() + public async Task Should_Fail_If_Assertion_Fails() { await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)); } [Fact] - public async void Should_Pass_If_Assertion_Fails() + public async Task Should_Pass_If_Assertion_Fails() { await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))); }