From 33154d2ed6b0b27f4a86a5fbad440a784a89c881 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 17 Jun 2024 13:33:54 -0400 Subject: [PATCH] feat!: add CancellationTokens, ValueTasks hooks (#268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is a combination of https://github.com/open-feature/dotnet-sdk/pull/184 and https://github.com/open-feature/dotnet-sdk/pull/185. Changes include: - adding cancellation tokens - in all cases where async operations include side-effects (`setProviderAsync`, `InitializeAsync`, I've specified in the in-line doc that the cancellation token's purpose is to cancel such side-effects - so setting a provider and canceling that operation still results in that provider's being set, but async side-effect should be cancelled. I'm interested in feedback here, I think we need to consider the semantics around this... I suppose the alternative would be to always ensure any state changes only occur after async side-effects, if they weren't cancelled beforehand. - adding "Async" suffix to all async methods - remove deprecated sync `SetProvider` methods - Using `ValueTask` for hook methods - I've decided against converting all `Tasks` to `ValueTasks`, from the [official .NET docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-8.0): > the default choice for any asynchronous method that does not return a result should be to return a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=net-8.0). Only if performance analysis proves it worthwhile should a ValueTask be used instead of a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=net-8.0). - I think for hooks, `ValueTask` especially makes sense since often hooks are synchronous, in fact async hooks are probably the less likely variant. - I've kept the resolver methods as `Task`, but there could be an argument for making them `ValueTask`, since some providers resolve asynchronously. - I'm still a bit dubious on the entire idea of `ValueTask`, so I'm really interested in feedback here - associated test updates UPDATE: After chewing on this for a night, I'm starting to feel: - We should simply remove cancellation tokens from Init/Shutdown. We can always add them later, which would be non-breaking. I think the value is low and the complexity is potentially high. - ValueTask is only a good idea for hooks, because: - Hooks will very often be synchronous under the hood - We (SDK authors) await the hooks, not consumer code, so we can be careful of the potential pitfalls of ValueTask. I think everywhere else we should stick to Task. --------- Signed-off-by: Austin Drenski Signed-off-by: Todd Baert Co-authored-by: Austin Drenski Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- README.md | 640 +++++++++--------- src/OpenFeature/Api.cs | 33 +- src/OpenFeature/EventExecutor.cs | 7 +- src/OpenFeature/FeatureProvider.cs | 34 +- src/OpenFeature/Hook.cs | 27 +- src/OpenFeature/IFeatureClient.cs | 31 +- src/OpenFeature/NoOpProvider.cs | 11 +- src/OpenFeature/OpenFeatureClient.cs | 118 ++-- src/OpenFeature/ProviderRepository.cs | 34 +- .../Providers/Memory/InMemoryProvider.cs | 28 +- .../OpenFeatureClientBenchmarks.cs | 30 +- .../Steps/EvaluationStepDefinitions.cs | 28 +- .../OpenFeature.Tests/FeatureProviderTests.cs | 38 +- .../OpenFeatureClientTests.cs | 146 ++-- .../OpenFeatureEventTests.cs | 22 +- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 359 ++++++---- test/OpenFeature.Tests/OpenFeatureTests.cs | 50 +- .../ProviderRepositoryTests.cs | 200 +++--- .../Providers/Memory/InMemoryProviderTests.cs | 51 +- test/OpenFeature.Tests/TestImplementations.cs | 49 +- test/OpenFeature.Tests/TestUtilsTest.cs | 5 +- 21 files changed, 1001 insertions(+), 940 deletions(-) diff --git a/README.md b/README.md index 6cb3c35c..6844915f 100644 --- a/README.md +++ b/README.md @@ -1,320 +1,320 @@ - - - -![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 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.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.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. +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.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.SetProviderAsync(new LocalProvider()); + +// registering a named provider +await Api.Instance.SetProviderAsync("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> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a string flag value + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a double 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 (`Before`/`After`/`Finally`/`Error`) need to be defined. + +```csharp +public class MyHook : Hook +{ + public ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary hints = null) + { + // code to run before flag evaluation + } + + public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null) + { + // code to run after successful flag evaluation + } + + 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 ValueTask FinallyAsync(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). + diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index fbafa695..6f13cac2 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -37,40 +37,17 @@ 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 - public async Task SetProviderAsync(FeatureProvider? featureProvider) + public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext()).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 @@ -85,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro 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()).ConfigureAwait(false); } /// @@ -248,7 +225,7 @@ public EvaluationContext GetContext() /// Once shut down is complete, API is reset and ready to use again. /// /// - public async Task Shutdown() + public async Task ShutdownAsync() { await using (this._eventExecutor.ConfigureAwait(false)) await using (this._repository.ConfigureAwait(false)) diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 816bf13e..886a47b6 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -10,6 +10,8 @@ namespace OpenFeature { + internal delegate Task ShutdownDelegate(CancellationToken cancellationToken); + internal sealed partial class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); @@ -30,7 +32,7 @@ public EventExecutor() eventProcessing.Start(); } - public ValueTask DisposeAsync() => new(this.Shutdown()); + public ValueTask DisposeAsync() => new(this.ShutdownAsync()); internal void SetLogger(ILogger logger) => this._logger = logger; @@ -317,10 +319,9 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } } - public async Task Shutdown() + public async Task ShutdownAsync() { this.EventChannel.Writer.Complete(); - await this.EventChannel.Reader.Completion.ConfigureAwait(false); } diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index bcc66558..62976f53 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 to cancel any async side effects. /// A task that completes when the initialization process is complete. /// /// @@ -118,7 +125,7 @@ public abstract Task> ResolveStructureValue(string flag /// the method after initialization is complete. /// /// - public virtual Task Initialize(EvaluationContext context) + public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { // Intentionally left blank. return Task.CompletedTask; @@ -129,7 +136,8 @@ 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 to cancel any async side effects. + public virtual Task ShutdownAsync(CancellationToken cancellationToken = default) { // Intentionally left blank. return Task.CompletedTask; 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..674b78a7 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 Task> 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 Task 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 Task 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 Task 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..7934da1c 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,7 +62,7 @@ public async ValueTask DisposeAsync() /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - public async Task SetProvider( + public async Task SetProviderAsync( FeatureProvider? featureProvider, EvaluationContext context, Action? afterSet = null, @@ -92,7 +92,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); #pragma warning restore CS4014 } finally @@ -100,11 +100,11 @@ public async Task SetProvider( this._providersLock.ExitWriteLock(); } - await InitProvider(this._defaultProvider, context, afterInitialization, afterError) + await InitProviderAsync(this._defaultProvider, context, afterInitialization, afterError) .ConfigureAwait(false); } - private static async Task InitProvider( + private static async Task InitProviderAsync( FeatureProvider? newProvider, EvaluationContext context, Action? afterInitialization, @@ -118,7 +118,7 @@ private static async Task InitProvider( { try { - await newProvider.Initialize(context).ConfigureAwait(false); + await newProvider.InitializeAsync(context).ConfigureAwait(false); afterInitialization?.Invoke(newProvider); } catch (Exception ex) @@ -152,13 +152,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 to cancel any async side effects. + public async Task 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 +189,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); #pragma warning restore CS4014 } finally @@ -195,13 +197,13 @@ public async Task SetProvider(string? clientName, this._providersLock.ExitWriteLock(); } - await InitProvider(featureProvider, context, afterInitialization, afterError).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 Task ShutdownIfUnused( + private async Task ShutdownIfUnusedAsync( FeatureProvider? targetProvider, Action? afterShutdown, Action? afterError) @@ -216,7 +218,7 @@ private async Task ShutdownIfUnused( return; } - await SafeShutdownProvider(targetProvider, afterShutdown, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider, afterShutdown, afterError).ConfigureAwait(false); } /// @@ -228,7 +230,7 @@ private async Task ShutdownIfUnused( /// it would not be meaningful to emit an error. /// /// - private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, + private static async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider, Action? afterShutdown, Action? afterError) { @@ -239,7 +241,7 @@ private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, try { - await targetProvider.Shutdown().ConfigureAwait(false); + await targetProvider.ShutdownAsync().ConfigureAwait(false); afterShutdown?.Invoke(targetProvider); } catch (Exception ex) @@ -281,7 +283,7 @@ public FeatureProvider GetProvider(string? clientName) : this.GetProvider(); } - public async Task Shutdown(Action? afterError = null) + public async Task ShutdownAsync(Action? afterError = null, CancellationToken cancellationToken = default) { var providers = new HashSet(); this._providersLock.EnterWriteLock(); @@ -305,7 +307,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).ConfigureAwait(false); } } } 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.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..a50f3945 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!, EvaluationContext.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..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; @@ -78,25 +79,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 +126,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 +169,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 +195,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 +217,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 +239,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 +261,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 +283,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 +306,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,26 +331,55 @@ 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] - public async Task Should_Use_No_Op_When_Provider_Is_Null() + public async Task Cancellation_Token_Added_Is_Passed_To_Provider() { - await Api.Instance.SetProviderAsync(null); - var client = new FeatureClient("test", "test"); - (await client.GetIntegerValue("some-key", 12)).Should().Be(12); + 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] diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 3a373c98..384928d6 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() @@ -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); @@ -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..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; @@ -35,18 +37,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 +57,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 +141,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 +197,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 +240,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 +266,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 +306,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 +326,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 +373,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 +412,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 +449,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 +487,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 +524,101 @@ 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] + 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] diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index c34a013d..673c183d 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -32,13 +31,13 @@ public async Task OpenFeature_Should_Initialize_Provider() providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerMockDefault); - await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()); + await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()); 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 providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()); } [Fact] @@ -50,27 +49,27 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() providerA.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerA); - await providerA.Received(1).Initialize(Api.Instance.GetContext()); + await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()); 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 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); - await providerC.Received(1).Initialize(Api.Instance.GetContext()); + await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()); 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 providerD.Received(1).InitializeAsync(Api.Instance.GetContext()); + await providerC.Received(1).ShutdownAsync(); } [Fact] @@ -86,10 +85,10 @@ public async Task OpenFeature_Should_Support_Shutdown() await Api.Instance.SetProviderAsync(providerA); await Api.Instance.SetProviderAsync("named", providerB); - await Api.Instance.Shutdown(); + await Api.Instance.ShutdownAsync(); - await providerA.Received(1).Shutdown(); - await providerB.Received(1).Shutdown(); + await providerA.Received(1).ShutdownAsync(); + await providerB.Received(1).ShutdownAsync(); } [Fact] @@ -128,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(true); - await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()).ConfigureAwait(true); + await openFeature.SetProviderAsync(name, new TestProvider()); + await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); } @@ -141,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(true); - await openFeature.SetProviderAsync("b", provider).ConfigureAwait(true); + await openFeature.SetProviderAsync("a", provider); + await openFeature.SetProviderAsync("b", provider); var clientA = openFeature.GetProvider("a"); var clientB = openFeature.GetProvider("b"); @@ -233,8 +232,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()); + await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()); var client1 = openFeature.GetClient("client1"); var client2 = openFeature.GetClient("client2"); @@ -242,19 +241,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"); + (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 0b25ebfa..ccec89bd 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -21,19 +21,19 @@ 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); + await repository.SetProviderAsync(provider, context); Assert.Equal(provider, repository.GetProvider()); } [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(); 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) => + await repository.SetProviderAsync(provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -48,9 +48,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 +61,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 +76,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 +99,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 +114,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 +129,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 +146,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 +161,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 +187,8 @@ 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); + + await repository.SetProviderAsync("the-name", provider, context); Assert.Equal(provider, repository.GetProvider("the-name")); } @@ -199,7 +200,7 @@ 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) => + await repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -214,9 +215,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 +228,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 +243,10 @@ 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; 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 +266,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 +281,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 +297,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 +314,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 +329,17 @@ 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; 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 +361,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 +381,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 +401,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 +422,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 +441,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 +462,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 +481,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 +520,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 +532,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 +547,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,33 +567,12 @@ 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")); } - - [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.SetProvider(defaultProvider, context); - - var namedProvider = Substitute.For(); - namedProvider.GetStatus().Returns(ProviderStatus.NotReady); - - await repository.SetProvider(null, namedProvider, context); - - namedProvider.DidNotReceive().Initialize(context); - namedProvider.DidNotReceive().Shutdown(); - - 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..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; @@ -109,45 +110,45 @@ public InMemoryProviderTests() } [Fact] - public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() + public async Task 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); } [Fact] - public async void GetString_ShouldEvaluateWithReasonAndVariant() + public async Task 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); } [Fact] - public async void GetInt_ShouldEvaluateWithReasonAndVariant() + public async Task 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); } [Fact] - public async void GetDouble_ShouldEvaluateWithReasonAndVariant() + public async Task 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); } [Fact] - public async void GetStruct_ShouldEvaluateWithReasonAndVariant() + public async Task 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); @@ -156,17 +157,17 @@ 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.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); } [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.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)); + 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.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)); + 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.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)); + 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.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)); + 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(){ { @@ -211,7 +212,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 +230,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 cdb59a0c..c949b373 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 Task 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); } } } 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))); }