diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7227000a..96d64160 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/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..cd4e643a
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs
@@ -0,0 +1,20 @@
+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.
+///
+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..b54b611a
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
@@ -0,0 +1,267 @@
+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.
+///
+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.
+ 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.
+ 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.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null)
+ where TOptions : OpenFeatureOptions, new()
+ where TProviderFactory : class, IFeatureProviderFactory, new()
+ {
+ 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.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null)
+ where TProviderFactory : class, IFeatureProviderFactory, new()
+ => 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.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null)
+ where TOptions : OpenFeatureOptions, new()
+ where TProviderFactory : class, IFeatureProviderFactory, new()
+ {
+ 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.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null)
+ where TProviderFactory : class, IFeatureProviderFactory, new()
+ => 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.
+ 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..7455fe2f
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
@@ -0,0 +1,67 @@
+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
+ 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..a214f174
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
@@ -0,0 +1,52 @@
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.DependencyInjection.Providers.Memory;
+
+///
+/// Extension methods for configuring feature providers with .
+///
+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..4841a4df
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs
@@ -0,0 +1,31 @@
+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.
+///
+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/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..1ca31cc1
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs
@@ -0,0 +1,6 @@
+namespace OpenFeature.DependencyInjection.Tests;
+
+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..0921ac46
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs
@@ -0,0 +1,92 @@
+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.");
+ }
+
+ [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.");
+ }
+
+ [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());
+ }
+}