diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7227000a..8e4347e2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,17 +1,19 @@
-
+
true
-
+
+
+
-
+
@@ -26,10 +28,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/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs
new file mode 100644
index 00000000..2085bda4
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs
@@ -0,0 +1,24 @@
+namespace OpenFeature;
+
+///
+/// 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/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs
new file mode 100644
index 00000000..9472b4f9
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs
@@ -0,0 +1,43 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace OpenFeature.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 featureProvider = _serviceProvider.GetService();
+ if (featureProvider == null)
+ {
+ throw new InvalidOperationException("Feature provider is not registered in the service collection.");
+ }
+ await _featureApi.SetProviderAsync(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/Guard.cs b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs
new file mode 100644
index 00000000..8d2726b9
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs
@@ -0,0 +1,14 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace OpenFeature;
+
+[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);
+ }
+}
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..fcdfd6ad
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj
@@ -0,0 +1,31 @@
+
+
+
+ netstandard2.0;net6.0;net8.0;net462
+ enable
+ enable
+ OpenFeature
+
+
+
+
+
+
+
+
+
+
+
+
+ <_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..6d55dcd0
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs
@@ -0,0 +1,20 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace OpenFeature;
+
+///
+/// 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; }
+}
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
new file mode 100644
index 00000000..8cc8e07a
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
@@ -0,0 +1,65 @@
+using Microsoft.Extensions.DependencyInjection.Extensions;
+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 feature provider to the service collection.
+ ///
+ /// The type of the feature provider, which must inherit from .
+ /// The instance.
+ /// A factory method to create the feature provider, using the service provider.
+ /// The instance.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func providerFactory)
+ where T : FeatureProvider
+ {
+ Guard.ThrowIfNull(builder);
+ builder.Services.TryAddSingleton(providerFactory);
+ return builder;
+ }
+}
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
new file mode 100644
index 00000000..28fb000f
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
@@ -0,0 +1,53 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using OpenFeature.Internal;
+using OpenFeature.Model;
+
+namespace OpenFeature;
+
+///
+/// Contains extension methods for the class.
+///
+public static class OpenFeatureServiceCollectionExtensions
+{
+ ///
+ /// This method is used to add OpenFeature to the service collection.
+ /// OpenFeature will be registered as a singleton.
+ ///
+ ///
+ /// the desired configuration
+ /// the current instance
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure)
+ {
+ Guard.ThrowIfNull(services);
+ Guard.ThrowIfNull(configure);
+
+ services.TryAddSingleton(Api.Instance);
+ services.TryAddSingleton();
+
+ var builder = new OpenFeatureBuilder(services);
+ configure(builder);
+
+ if (builder.IsContextConfigured)
+ {
+ services.TryAddScoped(static provider =>
+ {
+ var api = provider.GetRequiredService();
+ var client = api.GetClient();
+ var context = provider.GetRequiredService();
+ client.SetContext(context);
+ return client;
+ });
+ }
+ else
+ {
+ services.TryAddScoped(static provider =>
+ {
+ var api = provider.GetRequiredService();
+ return api.GetClient();
+ });
+ }
+
+ return services;
+ }
+}
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..dd9bcbee
--- /dev/null
+++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
@@ -0,0 +1,99 @@
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace OpenFeature;
+
+///
+/// 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..384d0471
--- /dev/null
+++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
@@ -0,0 +1,36 @@
+using Microsoft.Extensions.DependencyInjection;
+
+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..c18495d3
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs
@@ -0,0 +1,55 @@
+using FluentAssertions;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+using OpenFeature.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();
+
+ _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.Message.Should().Be("Feature provider is not registered in the service collection.");
+ }
+}
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/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..d4726db5
--- /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(_ => new NoOpFeatureProvider());
+
+ // 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(_ => new NoOpFeatureProvider());
+
+ 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..4149b8a8
--- /dev/null
+++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs
@@ -0,0 +1,40 @@
+using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Xunit;
+
+namespace OpenFeature.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().HaveCount(3);
+ _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());
+ }
+}