diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 895bf0e3..969d3dbf 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "2.0.0"
+ ".": "2.1.0"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb316f5d..038e2383 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
# Changelog
+## [2.1.0](https://github.com/open-feature/dotnet-sdk/compare/v2.0.0...v2.1.0) (2024-11-18)
+
+
+### 🐛 Bug Fixes
+
+* Fix action syntax in workflow configuration ([#315](https://github.com/open-feature/dotnet-sdk/issues/315)) ([ccf0250](https://github.com/open-feature/dotnet-sdk/commit/ccf02506ecd924738b6ae03dedf25c8e2df6d1fb))
+* Fix unit test clean context ([#313](https://github.com/open-feature/dotnet-sdk/issues/313)) ([3038142](https://github.com/open-feature/dotnet-sdk/commit/30381423333c54e1df98d7721dd72697fc5406dc))
+
+
+### ✨ New Features
+
+* Add Dependency Injection and Hosting support for OpenFeature ([#310](https://github.com/open-feature/dotnet-sdk/issues/310)) ([1aaa0ec](https://github.com/open-feature/dotnet-sdk/commit/1aaa0ec0e75d5048554752db30193694f0999a4a))
+
+
+### 🧹 Chore
+
+* **deps:** update actions/upload-artifact action to v4.4.3 ([#292](https://github.com/open-feature/dotnet-sdk/issues/292)) ([9b693f7](https://github.com/open-feature/dotnet-sdk/commit/9b693f737f111ed878749f725dd4c831206b308a))
+* **deps:** update codecov/codecov-action action to v4.6.0 ([#306](https://github.com/open-feature/dotnet-sdk/issues/306)) ([4b92528](https://github.com/open-feature/dotnet-sdk/commit/4b92528bd56541ca3701bd4cf80467cdda80f046))
+* **deps:** update dependency dotnet-sdk to v8.0.401 ([#296](https://github.com/open-feature/dotnet-sdk/issues/296)) ([0bae29d](https://github.com/open-feature/dotnet-sdk/commit/0bae29d4771c4901e0c511b8d3587e6501e67ecd))
+* **deps:** update dependency fluentassertions to 6.12.2 ([#302](https://github.com/open-feature/dotnet-sdk/issues/302)) ([bc7e187](https://github.com/open-feature/dotnet-sdk/commit/bc7e187b7586a04e0feb9ef28291ce14c9ac35c5))
+* **deps:** update dependency microsoft.net.test.sdk to 17.11.0 ([#297](https://github.com/open-feature/dotnet-sdk/issues/297)) ([5593e19](https://github.com/open-feature/dotnet-sdk/commit/5593e19ca990196f754cd0be69391abb8f0dbcd5))
+* **deps:** update dependency microsoft.net.test.sdk to 17.11.1 ([#301](https://github.com/open-feature/dotnet-sdk/issues/301)) ([5b979d2](https://github.com/open-feature/dotnet-sdk/commit/5b979d290d96020ffe7f3e5729550d6f988b2af2))
+* **deps:** update dependency nsubstitute to 5.3.0 ([#311](https://github.com/open-feature/dotnet-sdk/issues/311)) ([87f9cfa](https://github.com/open-feature/dotnet-sdk/commit/87f9cfa9b5ace84546690fea95f33bf06fd1947b))
+* **deps:** update dependency xunit to 2.9.2 ([#303](https://github.com/open-feature/dotnet-sdk/issues/303)) ([2273948](https://github.com/open-feature/dotnet-sdk/commit/22739486ee107562c72d02a46190c651e59a753c))
+* **deps:** update dotnet monorepo ([#305](https://github.com/open-feature/dotnet-sdk/issues/305)) ([3955b16](https://github.com/open-feature/dotnet-sdk/commit/3955b1604d5dad9b67e01974d96d53d5cacb9aad))
+* **deps:** update dotnet monorepo to 8.0.2 ([#319](https://github.com/open-feature/dotnet-sdk/issues/319)) ([94681f3](https://github.com/open-feature/dotnet-sdk/commit/94681f37821cc44388f0cd8898924cbfbcda0cd3))
+* update release please config ([#304](https://github.com/open-feature/dotnet-sdk/issues/304)) ([c471c06](https://github.com/open-feature/dotnet-sdk/commit/c471c062cf70d78b67f597f468c62dbfbf0674d2))
+
## [2.0.0](https://github.com/open-feature/dotnet-sdk/compare/v1.5.0...v2.0.0) (2024-08-21)
Today we're announcing the release of the OpenFeature SDK for .NET, v2.0! This release contains several ergonomic improvements to the SDK, which .NET developers will appreciate. It also includes some performance optimizations brought to you by the latest .NET primitives.
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ef5cde51..6db91397 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,17 +1,20 @@
-
+
true
-
+
+
+
+
-
+
@@ -26,10 +29,11 @@
+
-
+
-
+
diff --git a/OpenFeature.sln b/OpenFeature.sln
index 6f1cce8d..e8191acd 100644
--- a/OpenFeature.sln
+++ b/OpenFeature.sln
@@ -77,7 +77,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -101,21 +107,36 @@ Global
{7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
+ {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
+ {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
+ {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
+ {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
+ {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
{49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
- {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
- {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
- {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
- {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
- {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
- {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
+ {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
+ {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
+ {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F}
diff --git a/README.md b/README.md
index b8f25012..acb31e8f 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@
[![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=v2.0.0&color=blue&style=for-the-badge)
-](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.0.0)
+ ![Release](https://img.shields.io/static/v1?label=release&message=v2.1.0&color=blue&style=for-the-badge)
+](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.1.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)
@@ -79,8 +79,9 @@ public async Task Example()
| ✅ | [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. |
+| 🔬 | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. |
-> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌
+> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ | Experimental: 🔬
### Providers
@@ -300,6 +301,80 @@ public class MyHook : Hook
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!
+### DependencyInjection
+> [!NOTE]
+> The OpenFeature.DependencyInjection and OpenFeature.Hosting packages are currently experimental. They streamline the integration of OpenFeature within .NET applications, allowing for seamless configuration and lifecycle management of feature flag providers using dependency injection and hosting services.
+
+#### Installation
+To set up dependency injection and hosting capabilities for OpenFeature, install the following packages:
+```sh
+dotnet add package OpenFeature.DependencyInjection
+dotnet add package OpenFeature.Hosting
+```
+#### Usage Examples
+For a basic configuration, you can use the InMemoryProvider. This provider is simple and well-suited for development and testing purposes.
+
+**Basic Configuration:**
+```csharp
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder
+ .AddHostedFeatureLifecycle() // From Hosting package
+ .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ })
+ .AddInMemoryProvider();
+});
+```
+**Domain-Scoped Provider Configuration:**
+
To set up multiple providers with a selection policy, define logic for choosing the default provider. This example designates `name1` as the default provider:
+```csharp
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder
+ .AddHostedFeatureLifecycle()
+ .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ })
+ .AddInMemoryProvider("name1")
+ .AddInMemoryProvider("name2")
+ .AddPolicyName(options => {
+ // Custom logic to select a default provider
+ options.DefaultNameSelector = serviceProvider => "name1";
+ });
+});
+```
+#### Creating a New Provider
+To integrate a custom provider, such as InMemoryProvider, you’ll need to create a factory that builds and configures the provider. This section demonstrates how to set up InMemoryProvider as a new provider with custom configuration options.
+
+**Configuring InMemoryProvider as a New Provider**
+
Begin by creating a custom factory class, `InMemoryProviderFactory`, that implements `IFeatureProviderFactory`. This factory will initialize your provider with any necessary configurations.
+```csharp
+public class InMemoryProviderFactory : IFeatureProviderFactory
+{
+ internal IDictionary? Flags { get; set; }
+
+ public FeatureProvider Create() => new InMemoryProvider(Flags);
+}
+```
+**Adding an Extension Method to OpenFeatureBuilder**
+
To streamline the configuration process, add an extension method, `AddInMemoryProvider`, to `OpenFeatureBuilder`. This allows you to set up the provider with either a domain-scoped or a default configuration.
+
+```csharp
+public static partial class FeatureBuilderExtensions
+{
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null)
+ => builder.AddProvider(factory => ConfigureFlags(factory, configure));
+
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null)
+ => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure));
+
+ private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure)
+ {
+ if (configure == null)
+ return;
+
+ var flag = new Dictionary();
+ configure.Invoke(flag);
+ factory.Flags = flag;
+ }
+}
+```
+
## ⭐️ Support the project
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 656f3476..21a7efd6 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -9,7 +9,7 @@
- 2.0.0
+ 2.1.0
git
https://github.com/open-feature/dotnet-sdk
OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings.
diff --git a/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs b/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs
new file mode 100644
index 00000000..582ab39c
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs
@@ -0,0 +1,38 @@
+namespace OpenFeature.DependencyInjection.Diagnostics;
+
+///
+/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework.
+///
+///
+/// Experimental - This class includes identifiers that allow developers to track and conditionally enable
+/// experimental features. Each identifier follows a structured code format to indicate the feature domain,
+/// maturity level, and unique identifier. Note that experimental features are subject to change or removal
+/// in future releases.
+///
+/// Basic Information
+/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize
+/// and manage experimental features effectively.
+///
+///
+///
+///
+/// Code Structure:
+/// - "OF" - Represents the OpenFeature library.
+/// - "DI" - Indicates the Dependency Injection domain.
+/// - "001" - Unique identifier for a specific feature.
+///
+///
+internal static class FeatureCodes
+{
+ ///
+ /// Identifier for the experimental Dependency Injection features within the OpenFeature framework.
+ ///
+ ///
+ /// OFDI001 identifier marks experimental features in the Dependency Injection (DI) domain.
+ ///
+ /// Usage:
+ /// Developers can use this identifier to conditionally enable or test experimental DI features.
+ /// It is part of the OpenFeature diagnostics system to help track experimental functionality.
+ ///
+ public const string NewDi = "OFDI001";
+}
diff --git a/src/OpenFeature.DependencyInjection/Guard.cs b/src/OpenFeature.DependencyInjection/Guard.cs
new file mode 100644
index 00000000..337a8290
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Guard.cs
@@ -0,0 +1,20 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace OpenFeature.DependencyInjection;
+
+[DebuggerStepThrough]
+internal static class Guard
+{
+ public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
+ {
+ if (argument is null)
+ throw new ArgumentNullException(paramName);
+ }
+
+ public static void ThrowIfNullOrWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
+ {
+ if (string.IsNullOrWhiteSpace(argument))
+ throw new ArgumentNullException(paramName);
+ }
+}
diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs
new file mode 100644
index 00000000..4891f2e8
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs
@@ -0,0 +1,24 @@
+namespace OpenFeature.DependencyInjection;
+
+///
+/// Defines the contract for managing the lifecycle of a feature api.
+///
+public interface IFeatureLifecycleManager
+{
+ ///
+ /// Ensures that the feature provider is properly initialized and ready to be used.
+ /// This method should handle all necessary checks, configuration, and setup required to prepare the feature provider.
+ ///
+ /// Propagates notification that operations should be canceled.
+ /// A Task representing the asynchronous operation of initializing the feature provider.
+ /// Thrown when the feature provider is not registered or is in an invalid state.
+ ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Gracefully shuts down the feature api, ensuring all resources are properly disposed of and any persistent state is saved.
+ /// This method should handle all necessary cleanup and shutdown operations for the feature provider.
+ ///
+ /// Propagates notification that operations should be canceled.
+ /// A Task representing the asynchronous operation of shutting down the feature provider.
+ ValueTask ShutdownAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs
new file mode 100644
index 00000000..8c40cee3
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs
@@ -0,0 +1,23 @@
+namespace OpenFeature.DependencyInjection;
+
+///
+/// Provides a contract for creating instances of .
+/// This factory interface enables custom configuration and initialization of feature providers
+/// to support domain-specific or application-specific feature flag management.
+///
+#if NET8_0_OR_GREATER
+[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+public interface IFeatureProviderFactory
+{
+ ///
+ /// Creates an instance of a configured according to
+ /// the specific settings implemented by the concrete factory.
+ ///
+ ///
+ /// A new instance of .
+ /// The configuration and behavior of this provider instance are determined by
+ /// the implementation of this method.
+ ///
+ FeatureProvider Create();
+}
diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs
new file mode 100644
index 00000000..d14d421b
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs
@@ -0,0 +1,51 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace OpenFeature.DependencyInjection.Internal;
+
+internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager
+{
+ private readonly Api _featureApi;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+
+ public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger logger)
+ {
+ _featureApi = featureApi;
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ ///
+ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default)
+ {
+ this.LogStartingInitializationOfFeatureProvider();
+
+ var options = _serviceProvider.GetRequiredService>().Value;
+ if (options.HasDefaultProvider)
+ {
+ var featureProvider = _serviceProvider.GetRequiredService();
+ await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false);
+ }
+
+ foreach (var name in options.ProviderNames)
+ {
+ var featureProvider = _serviceProvider.GetRequiredKeyedService(name);
+ await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default)
+ {
+ this.LogShuttingDownFeatureProvider();
+ await _featureApi.ShutdownAsync().ConfigureAwait(false);
+ }
+
+ [LoggerMessage(200, LogLevel.Information, "Starting initialization of the feature provider")]
+ partial void LogStartingInitializationOfFeatureProvider();
+
+ [LoggerMessage(200, LogLevel.Information, "Shutting down the feature provider")]
+ partial void LogShuttingDownFeatureProvider();
+}
diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs
new file mode 100644
index 00000000..afbec6b0
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs
@@ -0,0 +1,23 @@
+// @formatter:off
+// ReSharper disable All
+#if NETCOREAPP3_0_OR_GREATER
+// https://github.com/dotnet/runtime/issues/96197
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))]
+#else
+#pragma warning disable
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Runtime.CompilerServices;
+
+[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
+internal sealed class CallerArgumentExpressionAttribute : Attribute
+{
+ public CallerArgumentExpressionAttribute(string parameterName)
+ {
+ ParameterName = parameterName;
+ }
+
+ public string ParameterName { get; }
+}
+#endif
diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs
new file mode 100644
index 00000000..87714111
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs
@@ -0,0 +1,21 @@
+// @formatter:off
+// ReSharper disable All
+#if NET5_0_OR_GREATER
+// https://github.com/dotnet/runtime/issues/96197
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
+#else
+#pragma warning disable
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel;
+
+namespace System.Runtime.CompilerServices;
+
+///
+/// Reserved to be used by the compiler for tracking metadata.
+/// This class should not be used by developers in source code.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+static class IsExternalInit { }
+#endif
diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj
new file mode 100644
index 00000000..895c45f3
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netstandard2.0;net6.0;net8.0;net462
+ enable
+ enable
+ OpenFeature.DependencyInjection
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_Parameter1>$(AssemblyName).Tests
+
+
+ <_Parameter1>DynamicProxyGenAssembly2
+
+
+
+
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs
new file mode 100644
index 00000000..ae1e8c8f
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs
@@ -0,0 +1,60 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace OpenFeature.DependencyInjection;
+
+///
+/// Describes a backed by an .
+///
+/// The services being configured.
+public class OpenFeatureBuilder(IServiceCollection services)
+{
+ /// The services being configured.
+ public IServiceCollection Services { get; } = services;
+
+ ///
+ /// Indicates whether the evaluation context has been configured.
+ /// This property is used to determine if specific configurations or services
+ /// should be initialized based on the presence of an evaluation context.
+ ///
+ public bool IsContextConfigured { get; internal set; }
+
+ ///
+ /// Indicates whether the policy has been configured.
+ ///
+ public bool IsPolicyConfigured { get; internal set; }
+
+ ///
+ /// Gets a value indicating whether a default provider has been registered.
+ ///
+ public bool HasDefaultProvider { get; internal set; }
+
+ ///
+ /// Gets the count of domain-bound providers that have been registered.
+ /// This count does not include the default provider.
+ ///
+ public int DomainBoundProviderRegistrationCount { get; internal set; }
+
+ ///
+ /// Validates the current configuration, ensuring that a policy is set when multiple providers are registered
+ /// or when a default provider is registered alongside another provider.
+ ///
+ ///
+ /// Thrown if multiple providers are registered without a policy, or if both a default provider
+ /// and an additional provider are registered without a policy configuration.
+ ///
+ public void Validate()
+ {
+ if (!IsPolicyConfigured)
+ {
+ if (DomainBoundProviderRegistrationCount > 1)
+ {
+ throw new InvalidOperationException("Multiple providers have been registered, but no policy has been configured.");
+ }
+
+ if (HasDefaultProvider && DomainBoundProviderRegistrationCount == 1)
+ {
+ throw new InvalidOperationException("A default provider and an additional provider have been registered without a policy configuration.");
+ }
+ }
+ }
+}
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
new file mode 100644
index 00000000..a494b045
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
@@ -0,0 +1,280 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using OpenFeature.DependencyInjection;
+using OpenFeature.Model;
+
+namespace OpenFeature;
+
+///
+/// Contains extension methods for the class.
+///
+#if NET8_0_OR_GREATER
+[System.Diagnostics.CodeAnalysis.Experimental(DependencyInjection.Diagnostics.FeatureCodes.NewDi)]
+#endif
+public static partial class OpenFeatureBuilderExtensions
+{
+ ///
+ /// This method is used to add a new context to the service collection.
+ ///
+ /// The instance.
+ /// the desired configuration
+ /// The instance.
+ /// Thrown when the or action is null.
+ public static OpenFeatureBuilder AddContext(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ Guard.ThrowIfNull(builder);
+ Guard.ThrowIfNull(configure);
+
+ return builder.AddContext((b, _) => configure(b));
+ }
+
+ ///
+ /// This method is used to add a new context to the service collection.
+ ///
+ /// The instance.
+ /// the desired configuration
+ /// The instance.
+ /// Thrown when the or action is null.
+ public static OpenFeatureBuilder AddContext(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ Guard.ThrowIfNull(builder);
+ Guard.ThrowIfNull(configure);
+
+ builder.IsContextConfigured = true;
+ builder.Services.TryAddTransient(provider =>
+ {
+ var contextBuilder = EvaluationContext.Builder();
+ configure(contextBuilder, provider);
+ return contextBuilder.Build();
+ });
+
+ return builder;
+ }
+
+ ///
+ /// Adds a new feature provider with specified options and configuration builder.
+ ///
+ /// The type for configuring the feature provider.
+ /// The type of the provider factory implementing .
+ /// The instance.
+ /// An optional action to configure the provider factory of type .
+ /// The instance.
+ /// Thrown when the is null.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null)
+ where TOptions : OpenFeatureOptions
+ where TProviderFactory : class, IFeatureProviderFactory
+ {
+ Guard.ThrowIfNull(builder);
+
+ builder.HasDefaultProvider = true;
+
+ builder.Services.Configure(options =>
+ {
+ options.AddDefaultProviderName();
+ });
+
+ if (configureFactory != null)
+ {
+ builder.Services.AddOptions()
+ .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.")
+ .Configure(configureFactory);
+ }
+ else
+ {
+ builder.Services.AddOptions()
+ .Configure(options => { });
+ }
+
+ builder.Services.TryAddSingleton(static provider =>
+ {
+ var providerFactory = provider.GetRequiredService>().Value;
+ return providerFactory.Create();
+ });
+
+ builder.AddClient();
+
+ return builder;
+ }
+
+ ///
+ /// Adds a new feature provider with the default type and a specified configuration builder.
+ ///
+ /// The type of the provider factory implementing .
+ /// The instance.
+ /// An optional action to configure the provider factory of type .
+ /// The configured instance.
+ /// Thrown when the is null.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null)
+ where TProviderFactory : class, IFeatureProviderFactory
+ => AddProvider(builder, configureFactory);
+
+ ///
+ /// Adds a feature provider with specified options and configuration builder for the specified domain.
+ ///
+ /// The type for configuring the feature provider.
+ /// The type of the provider factory implementing .
+ /// The instance.
+ /// The unique name of the provider.
+ /// An optional action to configure the provider factory of type .
+ /// The instance.
+ /// Thrown when the or is null or empty.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null)
+ where TOptions : OpenFeatureOptions
+ where TProviderFactory : class, IFeatureProviderFactory
+ {
+ Guard.ThrowIfNull(builder);
+ Guard.ThrowIfNullOrWhiteSpace(domain, nameof(domain));
+
+ builder.DomainBoundProviderRegistrationCount++;
+
+ builder.Services.Configure(options =>
+ {
+ options.AddProviderName(domain);
+ });
+
+ if (configureFactory != null)
+ {
+ builder.Services.AddOptions(domain)
+ .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.")
+ .Configure(configureFactory);
+ }
+ else
+ {
+ builder.Services.AddOptions(domain)
+ .Configure(options => { });
+ }
+
+ builder.Services.TryAddKeyedSingleton(domain, static (provider, key) =>
+ {
+ var options = provider.GetRequiredService>();
+ var providerFactory = options.Get(key!.ToString());
+ return providerFactory.Create();
+ });
+
+ builder.AddClient(domain);
+
+ return builder;
+ }
+
+ ///
+ /// Adds a feature provider with a specified configuration builder for the specified domain, using default .
+ ///
+ /// The type of the provider factory implementing .
+ /// The instance.
+ /// The unique domain of the provider.
+ /// An optional action to configure the provider factory of type .
+ /// The configured instance.
+ /// Thrown when the or is null or empty.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null)
+ where TProviderFactory : class, IFeatureProviderFactory
+ => AddProvider(builder, domain, configureFactory);
+
+ ///
+ /// Adds a feature client to the service collection, configuring it to work with a specific context if provided.
+ ///
+ /// The instance.
+ /// Optional: The name for the feature client instance.
+ /// The instance.
+ internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, string? name = null)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ if (builder.IsContextConfigured)
+ {
+ builder.Services.TryAddScoped(static provider =>
+ {
+ var api = provider.GetRequiredService();
+ var client = api.GetClient();
+ var context = provider.GetRequiredService();
+ client.SetContext(context);
+ return client;
+ });
+ }
+ else
+ {
+ builder.Services.TryAddScoped(static provider =>
+ {
+ var api = provider.GetRequiredService();
+ return api.GetClient();
+ });
+ }
+ }
+ else
+ {
+ if (builder.IsContextConfigured)
+ {
+ builder.Services.TryAddKeyedScoped(name, static (provider, key) =>
+ {
+ var api = provider.GetRequiredService();
+ var client = api.GetClient(key!.ToString());
+ var context = provider.GetRequiredService();
+ client.SetContext(context);
+ return client;
+ });
+ }
+ else
+ {
+ builder.Services.TryAddKeyedScoped(name, static (provider, key) =>
+ {
+ var api = provider.GetRequiredService();
+ return api.GetClient(key!.ToString());
+ });
+ }
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Configures a default client for OpenFeature using the provided factory function.
+ ///
+ /// The instance.
+ ///
+ /// A factory function that creates an based on the service provider and .
+ ///
+ /// The configured instance.
+ internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func clientFactory)
+ {
+ builder.Services.AddScoped(provider =>
+ {
+ var policy = provider.GetRequiredService>().Value;
+ return clientFactory(provider, policy);
+ });
+
+ return builder;
+ }
+
+ ///
+ /// Configures policy name options for OpenFeature using the specified options type.
+ ///
+ /// The type of options used to configure .
+ /// The instance.
+ /// A delegate to configure .
+ /// The configured instance.
+ /// Thrown when the or is null.
+ public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions)
+ where TOptions : PolicyNameOptions
+ {
+ Guard.ThrowIfNull(builder);
+ Guard.ThrowIfNull(configureOptions);
+
+ builder.IsPolicyConfigured = true;
+
+ builder.Services.Configure(configureOptions);
+ return builder;
+ }
+
+ ///
+ /// Configures the default policy name options for OpenFeature.
+ ///
+ /// The instance.
+ /// A delegate to configure .
+ /// The configured instance.
+ public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions)
+ => AddPolicyName(builder, configureOptions);
+}
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
new file mode 100644
index 00000000..1be312ed
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
@@ -0,0 +1,49 @@
+namespace OpenFeature.DependencyInjection;
+
+///
+/// Options to configure OpenFeature
+///
+public class OpenFeatureOptions
+{
+ private readonly HashSet _providerNames = [];
+
+ ///
+ /// Determines if a default provider has been registered.
+ ///
+ public bool HasDefaultProvider { get; private set; }
+
+ ///
+ /// The type of the configured feature provider.
+ ///
+ public Type FeatureProviderType { get; protected internal set; } = null!;
+
+ ///
+ /// Gets a read-only list of registered provider names.
+ ///
+ public IReadOnlyCollection ProviderNames => _providerNames;
+
+ ///
+ /// Registers the default provider name if no specific name is provided.
+ /// Sets to true.
+ ///
+ public void AddDefaultProviderName() => AddProviderName(null);
+
+ ///
+ /// Registers a new feature provider name. This operation is thread-safe.
+ ///
+ /// The name of the feature provider to register. Registers as default if null.
+ public void AddProviderName(string? name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ HasDefaultProvider = true;
+ }
+ else
+ {
+ lock (_providerNames)
+ {
+ _providerNames.Add(name!);
+ }
+ }
+ }
+}
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
new file mode 100644
index 00000000..e7a503bb
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
@@ -0,0 +1,68 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using OpenFeature.DependencyInjection;
+using OpenFeature.DependencyInjection.Internal;
+
+namespace OpenFeature;
+
+///
+/// Contains extension methods for the class.
+///
+public static partial class OpenFeatureServiceCollectionExtensions
+{
+ ///
+ /// Adds and configures OpenFeature services to the provided .
+ ///
+ /// The instance.
+ /// A configuration action for customizing OpenFeature setup via
+ /// The modified instance
+ /// Thrown if or is null.
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure)
+ {
+ Guard.ThrowIfNull(services);
+ Guard.ThrowIfNull(configure);
+
+ // Register core OpenFeature services as singletons.
+ services.TryAddSingleton(Api.Instance);
+ services.TryAddSingleton();
+
+ var builder = new OpenFeatureBuilder(services);
+ configure(builder);
+
+ // If a default provider is specified without additional providers,
+ // return early as no extra configuration is needed.
+ if (builder.HasDefaultProvider && builder.DomainBoundProviderRegistrationCount == 0)
+ {
+ return services;
+ }
+
+ // Validate builder configuration to ensure consistency and required setup.
+ builder.Validate();
+
+ if (!builder.IsPolicyConfigured)
+ {
+ // Add a default name selector policy to use the first registered provider name as the default.
+ builder.AddPolicyName(options =>
+ {
+ options.DefaultNameSelector = provider =>
+ {
+ var options = provider.GetRequiredService>().Value;
+ return options.ProviderNames.First();
+ };
+ });
+ }
+
+ builder.AddDefaultClient((provider, policy) =>
+ {
+ var name = policy.DefaultNameSelector.Invoke(provider);
+ if (name == null)
+ {
+ return provider.GetRequiredService();
+ }
+ return provider.GetRequiredKeyedService(name);
+ });
+
+ return services;
+ }
+}
diff --git a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs
new file mode 100644
index 00000000..f77b019b
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs
@@ -0,0 +1,12 @@
+namespace OpenFeature.DependencyInjection;
+
+///
+/// Options to configure the default feature client name.
+///
+public class PolicyNameOptions
+{
+ ///
+ /// A delegate to select the default feature client name.
+ ///
+ public Func DefaultNameSelector { get; set; } = null!;
+}
diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
new file mode 100644
index 00000000..199e01b0
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
@@ -0,0 +1,55 @@
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.DependencyInjection.Providers.Memory;
+
+///
+/// Extension methods for configuring feature providers with .
+///
+#if NET8_0_OR_GREATER
+[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+public static partial class FeatureBuilderExtensions
+{
+ ///
+ /// Adds an in-memory feature provider to the with optional flag configuration.
+ ///
+ /// The instance to configure.
+ ///
+ /// An optional delegate to configure feature flags in the in-memory provider.
+ /// If provided, it allows setting up the initial flags.
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null)
+ => builder.AddProvider(factory => ConfigureFlags(factory, configure));
+
+ ///
+ /// Adds an in-memory feature provider with a specific domain to the
+ /// with optional flag configuration.
+ ///
+ /// The instance to configure.
+ /// The unique domain of the provider
+ ///
+ /// An optional delegate to configure feature flags in the in-memory provider.
+ /// If provided, it allows setting up the initial flags.
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null)
+ => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure));
+
+ ///
+ /// Configures the feature flags for an instance.
+ ///
+ /// The to configure.
+ ///
+ /// An optional delegate that sets up the initial flags in the provider's flag dictionary.
+ ///
+ private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure)
+ {
+ if (configure == null)
+ return;
+
+ var flag = new Dictionary();
+ configure.Invoke(flag);
+ factory.Flags = flag;
+ }
+}
diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs
new file mode 100644
index 00000000..2d155dd9
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs
@@ -0,0 +1,34 @@
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.DependencyInjection.Providers.Memory;
+
+///
+/// A factory for creating instances of ,
+/// an in-memory implementation of .
+/// This factory allows for the customization of feature flags to facilitate
+/// testing and lightweight feature flag management without external dependencies.
+///
+#if NET8_0_OR_GREATER
+[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+public class InMemoryProviderFactory : IFeatureProviderFactory
+{
+ ///
+ /// Gets or sets the collection of feature flags used to configure the
+ /// instances. This dictionary maps
+ /// flag names to instances, enabling pre-configuration
+ /// of features for testing or in-memory evaluation.
+ ///
+ internal IDictionary? Flags { get; set; }
+
+ ///
+ /// Creates a new instance of with the specified
+ /// flags set in . This instance is configured for in-memory
+ /// feature flag management, suitable for testing or lightweight feature toggling scenarios.
+ ///
+ ///
+ /// A configured that can be used to manage
+ /// feature flags in an in-memory context.
+ ///
+ public FeatureProvider Create() => new InMemoryProvider(Flags);
+}
diff --git a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs
new file mode 100644
index 00000000..91e3047d
--- /dev/null
+++ b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs
@@ -0,0 +1,18 @@
+namespace OpenFeature;
+
+///
+/// Represents the lifecycle state options for a feature,
+/// defining the states during the start and stop lifecycle.
+///
+public class FeatureLifecycleStateOptions
+{
+ ///
+ /// Gets or sets the state during the feature startup lifecycle.
+ ///
+ public FeatureStartState StartState { get; set; } = FeatureStartState.Starting;
+
+ ///
+ /// Gets or sets the state during the feature shutdown lifecycle.
+ ///
+ public FeatureStopState StopState { get; set; } = FeatureStopState.Stopping;
+}
diff --git a/src/OpenFeature.Hosting/FeatureStartState.cs b/src/OpenFeature.Hosting/FeatureStartState.cs
new file mode 100644
index 00000000..8001b9c2
--- /dev/null
+++ b/src/OpenFeature.Hosting/FeatureStartState.cs
@@ -0,0 +1,22 @@
+namespace OpenFeature;
+
+///
+/// Defines the various states for starting a feature.
+///
+public enum FeatureStartState
+{
+ ///
+ /// The feature is in the process of starting.
+ ///
+ Starting,
+
+ ///
+ /// The feature is at the start state.
+ ///
+ Start,
+
+ ///
+ /// The feature has fully started.
+ ///
+ Started
+}
diff --git a/src/OpenFeature.Hosting/FeatureStopState.cs b/src/OpenFeature.Hosting/FeatureStopState.cs
new file mode 100644
index 00000000..d8d6a28c
--- /dev/null
+++ b/src/OpenFeature.Hosting/FeatureStopState.cs
@@ -0,0 +1,22 @@
+namespace OpenFeature;
+
+///
+/// Defines the various states for stopping a feature.
+///
+public enum FeatureStopState
+{
+ ///
+ /// The feature is in the process of stopping.
+ ///
+ Stopping,
+
+ ///
+ /// The feature is at the stop state.
+ ///
+ Stop,
+
+ ///
+ /// The feature has fully stopped.
+ ///
+ Stopped
+}
diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
new file mode 100644
index 00000000..5209a525
--- /dev/null
+++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
@@ -0,0 +1,100 @@
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OpenFeature.DependencyInjection;
+
+namespace OpenFeature.Hosting;
+
+///
+/// A hosted service that manages the lifecycle of features within the application.
+/// It ensures that features are properly initialized when the service starts
+/// and gracefully shuts down when the service stops.
+///
+public sealed partial class HostedFeatureLifecycleService : IHostedLifecycleService
+{
+ private readonly ILogger _logger;
+ private readonly IFeatureLifecycleManager _featureLifecycleManager;
+ private readonly IOptions _featureLifecycleStateOptions;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger used to log lifecycle events.
+ /// The feature lifecycle manager responsible for initialization and shutdown.
+ /// Options that define the start and stop states of the feature lifecycle.
+ public HostedFeatureLifecycleService(
+ ILogger logger,
+ IFeatureLifecycleManager featureLifecycleManager,
+ IOptions featureLifecycleStateOptions)
+ {
+ _logger = logger;
+ _featureLifecycleManager = featureLifecycleManager;
+ _featureLifecycleStateOptions = featureLifecycleStateOptions;
+ }
+
+ ///
+ /// Ensures that the feature is properly initialized when the service starts.
+ ///
+ public async Task StartingAsync(CancellationToken cancellationToken)
+ => await InitializeIfStateMatchesAsync(FeatureStartState.Starting, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Ensures that the feature is in the "Start" state.
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken)
+ => await InitializeIfStateMatchesAsync(FeatureStartState.Start, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Ensures that the feature is fully started and operational.
+ ///
+ public async Task StartedAsync(CancellationToken cancellationToken)
+ => await InitializeIfStateMatchesAsync(FeatureStartState.Started, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Gracefully shuts down the feature when the service is stopping.
+ ///
+ public async Task StoppingAsync(CancellationToken cancellationToken)
+ => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopping, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Ensures that the feature is in the "Stop" state.
+ ///
+ public async Task StopAsync(CancellationToken cancellationToken)
+ => await ShutdownIfStateMatchesAsync(FeatureStopState.Stop, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Ensures that the feature is fully stopped and no longer operational.
+ ///
+ public async Task StoppedAsync(CancellationToken cancellationToken)
+ => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopped, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Initializes the feature lifecycle if the current state matches the expected start state.
+ ///
+ private async Task InitializeIfStateMatchesAsync(FeatureStartState expectedState, CancellationToken cancellationToken)
+ {
+ if (_featureLifecycleStateOptions.Value.StartState == expectedState)
+ {
+ this.LogInitializingFeatureLifecycleManager(expectedState);
+ await _featureLifecycleManager.EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Shuts down the feature lifecycle if the current state matches the expected stop state.
+ ///
+ private async Task ShutdownIfStateMatchesAsync(FeatureStopState expectedState, CancellationToken cancellationToken)
+ {
+ if (_featureLifecycleStateOptions.Value.StopState == expectedState)
+ {
+ this.LogShuttingDownFeatureLifecycleManager(expectedState);
+ await _featureLifecycleManager.ShutdownAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ [LoggerMessage(200, LogLevel.Information, "Initializing the Feature Lifecycle Manager for state {State}.")]
+ partial void LogInitializingFeatureLifecycleManager(FeatureStartState state);
+
+ [LoggerMessage(200, LogLevel.Information, "Shutting down the Feature Lifecycle Manager for state {State}")]
+ partial void LogShuttingDownFeatureLifecycleManager(FeatureStopState state);
+}
diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
new file mode 100644
index 00000000..48730084
--- /dev/null
+++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net6.0;net8.0
+ enable
+ enable
+ OpenFeature
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
new file mode 100644
index 00000000..16f437b3
--- /dev/null
+++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
@@ -0,0 +1,38 @@
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature.DependencyInjection;
+using OpenFeature.Hosting;
+
+namespace OpenFeature;
+
+///
+/// Extension methods for configuring the hosted feature lifecycle in the .
+///
+public static partial class OpenFeatureBuilderExtensions
+{
+ ///
+ /// Adds the to the OpenFeatureBuilder,
+ /// which manages the lifecycle of features within the application. It also allows
+ /// configuration of the .
+ ///
+ /// The instance.
+ /// An optional action to configure .
+ /// The instance.
+ public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null)
+ {
+ if (configureOptions == null)
+ {
+ builder.Services.Configure(cfg =>
+ {
+ cfg.StartState = FeatureStartState.Starting;
+ cfg.StopState = FeatureStopState.Stopping;
+ });
+ }
+ else
+ {
+ builder.Services.Configure(configureOptions);
+ }
+
+ builder.Services.AddHostedService();
+ return builder;
+ }
+}
diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs
index 08e29533..c2621785 100644
--- a/src/OpenFeature/OpenFeatureClient.cs
+++ b/src/OpenFeature/OpenFeatureClient.cs
@@ -263,7 +263,23 @@ private async Task> EvaluateFlagAsync(
(await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false))
.ToFlagEvaluationDetails();
- await this.TriggerAfterHooksAsync(allHooksReversed, hookContext, evaluation, options, cancellationToken).ConfigureAwait(false);
+ if (evaluation.ErrorType == ErrorType.None)
+ {
+ await this.TriggerAfterHooksAsync(
+ allHooksReversed,
+ hookContext,
+ evaluation,
+ options,
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+ else
+ {
+ var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage);
+ this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception);
+ await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, options, cancellationToken)
+ .ConfigureAwait(false);
+ }
}
catch (FeatureProviderException ex)
{
diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs
new file mode 100644
index 00000000..b0176bc4
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs
@@ -0,0 +1,64 @@
+using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using NSubstitute;
+using OpenFeature.DependencyInjection.Internal;
+using Xunit;
+
+namespace OpenFeature.DependencyInjection.Tests;
+
+public class FeatureLifecycleManagerTests
+{
+ private readonly FeatureLifecycleManager _systemUnderTest;
+ private readonly IServiceProvider _mockServiceProvider;
+
+ public FeatureLifecycleManagerTests()
+ {
+ Api.Instance.SetContext(null);
+ Api.Instance.ClearHooks();
+
+ _mockServiceProvider = Substitute.For();
+
+ var options = new OpenFeatureOptions();
+ options.AddDefaultProviderName();
+ var optionsMock = Substitute.For>();
+ optionsMock.Value.Returns(options);
+
+ _mockServiceProvider.GetService>().Returns(optionsMock);
+
+ _systemUnderTest = new FeatureLifecycleManager(
+ Api.Instance,
+ _mockServiceProvider,
+ Substitute.For>());
+ }
+
+ [Fact]
+ public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExists()
+ {
+ // Arrange
+ var featureProvider = new NoOpFeatureProvider();
+ _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(featureProvider);
+
+ // Act
+ await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(true);
+
+ // Assert
+ Api.Instance.GetProvider().Should().BeSameAs(featureProvider);
+ }
+
+ [Fact]
+ public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist()
+ {
+ // Arrange
+ _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider);
+
+ // Act
+ var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask();
+
+ // Assert
+ var exception = await Assert.ThrowsAsync(act).ConfigureAwait(true);
+ exception.Should().NotBeNull();
+ exception.Message.Should().NotBeNullOrWhiteSpace();
+ }
+}
diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs
new file mode 100644
index 00000000..ac3e5209
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs
@@ -0,0 +1,52 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.DependencyInjection.Tests;
+
+// This class replicates the NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider.cs.
+// It is used here to facilitate unit testing without relying on the internal NoOpFeatureProvider class.
+// If the InternalsVisibleTo attribute is added to the OpenFeature project,
+// this class can be removed and the original NoOpFeatureProvider can be directly accessed for testing.
+internal sealed class NoOpFeatureProvider : FeatureProvider
+{
+ private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName);
+
+ public override Metadata GetMetadata()
+ {
+ return this._metadata;
+ }
+
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(NoOpResponse(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(NoOpResponse(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(NoOpResponse(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(NoOpResponse(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(NoOpResponse(flagKey, defaultValue));
+ }
+
+ private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue)
+ {
+ return new ResolutionDetails(
+ flagKey,
+ defaultValue,
+ reason: NoOpProvider.ReasonNoOp,
+ variant: NoOpProvider.Variant
+ );
+ }
+}
diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs
new file mode 100644
index 00000000..1ee14bf0
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs
@@ -0,0 +1,9 @@
+namespace OpenFeature.DependencyInjection.Tests;
+
+#if NET8_0_OR_GREATER
+[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+public class NoOpFeatureProviderFactory : IFeatureProviderFactory
+{
+ public FeatureProvider Create() => new NoOpFeatureProvider();
+}
diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs
new file mode 100644
index 00000000..7bf20bca
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs
@@ -0,0 +1,8 @@
+namespace OpenFeature.DependencyInjection.Tests;
+
+internal static class NoOpProvider
+{
+ public const string NoOpProviderName = "No-op Provider";
+ public const string ReasonNoOp = "No-op";
+ public const string Variant = "No-op";
+}
diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj
new file mode 100644
index 00000000..9937e1bc
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net6.0;net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs
new file mode 100644
index 00000000..3f6ef227
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs
@@ -0,0 +1,98 @@
+using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.DependencyInjection.Tests;
+
+public partial class OpenFeatureBuilderExtensionsTests
+{
+ private readonly IServiceCollection _services;
+ private readonly OpenFeatureBuilder _systemUnderTest;
+
+ public OpenFeatureBuilderExtensionsTests()
+ {
+ _services = new ServiceCollection();
+ _systemUnderTest = new OpenFeatureBuilder(_services);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate)
+ {
+ // Act
+ var result = useServiceProviderDelegate ?
+ _systemUnderTest.AddContext(_ => { }) :
+ _systemUnderTest.AddContext((_, _) => { });
+
+ // Assert
+ result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance.");
+ _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured.");
+ _services.Should().ContainSingle(serviceDescriptor =>
+ serviceDescriptor.ServiceType == typeof(EvaluationContext) &&
+ serviceDescriptor.Lifetime == ServiceLifetime.Transient,
+ "A transient service of type EvaluationContext should be added.");
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate)
+ {
+ // Arrange
+ bool delegateCalled = false;
+
+ _ = useServiceProviderDelegate ?
+ _systemUnderTest.AddContext(_ => delegateCalled = true) :
+ _systemUnderTest.AddContext((_, _) => delegateCalled = true);
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var context = serviceProvider.GetService();
+
+ // Assert
+ _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured.");
+ context.Should().NotBeNull("The EvaluationContext should be resolvable.");
+ delegateCalled.Should().BeTrue("The delegate should be invoked.");
+ }
+
+#if NET8_0_OR_GREATER
+ [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+ [Fact]
+ public void AddProvider_ShouldAddProviderToCollection()
+ {
+ // Act
+ var result = _systemUnderTest.AddProvider();
+
+ // Assert
+ _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured.");
+ result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance.");
+ _services.Should().ContainSingle(serviceDescriptor =>
+ serviceDescriptor.ServiceType == typeof(FeatureProvider) &&
+ serviceDescriptor.Lifetime == ServiceLifetime.Singleton,
+ "A singleton service of type FeatureProvider should be added.");
+ }
+
+#if NET8_0_OR_GREATER
+ [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+ [Fact]
+ public void AddProvider_ShouldResolveCorrectProvider()
+ {
+ // Arrange
+ _systemUnderTest.AddProvider();
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var provider = serviceProvider.GetService();
+
+ // Assert
+ _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured.");
+ provider.Should().NotBeNull("The FeatureProvider should be resolvable.");
+ provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider.");
+ }
+}
diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs
new file mode 100644
index 00000000..40e761d2
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs
@@ -0,0 +1,39 @@
+using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Xunit;
+
+namespace OpenFeature.DependencyInjection.Tests;
+
+public class OpenFeatureServiceCollectionExtensionsTests
+{
+ private readonly IServiceCollection _systemUnderTest;
+ private readonly Action _configureAction;
+
+ public OpenFeatureServiceCollectionExtensionsTests()
+ {
+ _systemUnderTest = new ServiceCollection();
+ _configureAction = Substitute.For>();
+ }
+
+ [Fact]
+ public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSingleton()
+ {
+ // Act
+ _systemUnderTest.AddOpenFeature(_configureAction);
+
+ _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton);
+ _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton);
+ _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped);
+ }
+
+ [Fact]
+ public void AddOpenFeature_ShouldInvokeConfigureAction()
+ {
+ // Act
+ _systemUnderTest.AddOpenFeature(_configureAction);
+
+ // Assert
+ _configureAction.Received(1).Invoke(Arg.Any());
+ }
+}
diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs
index cf17b4a0..002c86c6 100644
--- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs
+++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs
@@ -433,6 +433,41 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error()
_ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any());
}
+ [Fact]
+ public async Task When_Error_Is_Returned_From_Provider_Should_Not_Run_After_Hook_But_Error_Hook()
+ {
+ var fixture = new Fixture();
+ var domain = fixture.Create();
+ var clientVersion = fixture.Create();
+ var flagName = fixture.Create();
+ var defaultValue = fixture.Create();
+ const string testMessage = "Couldn't parse flag data.";
+
+ var featureProviderMock = Substitute.For();
+ 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(domain, clientVersion);
+ var testHook = new TestHook();
+ client.AddHooks(testHook);
+ 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)
+ .ResolveStructureValueAsync(flagName, defaultValue, Arg.Any());
+
+ Assert.Equal(1, testHook.BeforeCallCount);
+ Assert.Equal(0, testHook.AfterCallCount);
+ Assert.Equal(1, testHook.ErrorCallCount);
+ Assert.Equal(1, testHook.FinallyCallCount);
+ }
+
[Fact]
public async Task Cancellation_Token_Added_Is_Passed_To_Provider()
{
@@ -454,6 +489,7 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider()
{
await Task.Delay(10); // artificially delay until cancelled
}
+
return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason);
});
featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create()));
diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs
index 7a1dff10..ea35b870 100644
--- a/test/OpenFeature.Tests/TestImplementations.cs
+++ b/test/OpenFeature.Tests/TestImplementations.cs
@@ -8,28 +8,49 @@
namespace OpenFeature.Tests
{
- public class TestHookNoOverride : Hook { }
+ public class TestHookNoOverride : Hook
+ {
+ }
public class TestHook : Hook
{
- public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ private int _beforeCallCount;
+ public int BeforeCallCount { get => this._beforeCallCount; }
+
+ private int _afterCallCount;
+ public int AfterCallCount { get => this._afterCallCount; }
+
+ private int _errorCallCount;
+ public int ErrorCallCount { get => this._errorCallCount; }
+
+ private int _finallyCallCount;
+ public int FinallyCallCount { get => this._finallyCallCount; }
+
+ public override ValueTask BeforeAsync(HookContext context,
+ IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
{
+ Interlocked.Increment(ref this._beforeCallCount);
return new ValueTask(EvaluationContext.Empty);
}
public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details,
IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
{
+ Interlocked.Increment(ref this._afterCallCount);
return new ValueTask();
}
- public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ public override ValueTask ErrorAsync(HookContext context, Exception error,
+ IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
{
+ Interlocked.Increment(ref this._errorCallCount);
return new ValueTask();
}
- public override ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ public override ValueTask FinallyAsync(HookContext context,
+ IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
{
+ Interlocked.Increment(ref this._finallyCallCount);
return new ValueTask();
}
}
diff --git a/version.txt b/version.txt
index 227cea21..7ec1d6db 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-2.0.0
+2.1.0