diff --git a/OpenFeature.slnx b/OpenFeature.slnx
index d6778e50..05aaad11 100644
--- a/OpenFeature.slnx
+++ b/OpenFeature.slnx
@@ -62,6 +62,7 @@
+
diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs
index e8faf5a5..de0498f2 100644
--- a/samples/AspNetCore/Program.cs
+++ b/samples/AspNetCore/Program.cs
@@ -2,8 +2,8 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using OpenFeature;
-using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Hooks;
+using OpenFeature.Hosting.Providers.Memory;
using OpenFeature.Model;
using OpenFeature.Providers.Memory;
using OpenTelemetry.Metrics;
@@ -38,7 +38,7 @@
.WithFlagEvaluationMetadata("boolean", s => s.GetBool("boolean"))
.Build();
- featureBuilder.AddHostedFeatureLifecycle()
+ featureBuilder
.AddHook(sp => new LoggingHook(sp.GetRequiredService>()))
.AddHook(_ => new MetricsHook(metricsHookOptions))
.AddHook()
diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj
index b6223bd0..fd8791fc 100644
--- a/samples/AspNetCore/Samples.AspNetCore.csproj
+++ b/samples/AspNetCore/Samples.AspNetCore.csproj
@@ -7,7 +7,6 @@
-
diff --git a/src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs b/src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs
new file mode 100644
index 00000000..f7ecf81c
--- /dev/null
+++ b/src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs
@@ -0,0 +1,38 @@
+namespace OpenFeature.Hosting.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.Hosting/Guard.cs b/src/OpenFeature.Hosting/Guard.cs
new file mode 100644
index 00000000..2d37ef54
--- /dev/null
+++ b/src/OpenFeature.Hosting/Guard.cs
@@ -0,0 +1,14 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace OpenFeature.Hosting;
+
+[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.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
index 5209a525..4411c21b 100644
--- a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
+++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
@@ -1,7 +1,6 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using OpenFeature.DependencyInjection;
namespace OpenFeature.Hosting;
diff --git a/src/OpenFeature.Hosting/IFeatureLifecycleManager.cs b/src/OpenFeature.Hosting/IFeatureLifecycleManager.cs
new file mode 100644
index 00000000..54f791fb
--- /dev/null
+++ b/src/OpenFeature.Hosting/IFeatureLifecycleManager.cs
@@ -0,0 +1,24 @@
+namespace OpenFeature.Hosting;
+
+///
+/// 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.Hosting/Internal/EventHandlerDelegateWrapper.cs b/src/OpenFeature.Hosting/Internal/EventHandlerDelegateWrapper.cs
new file mode 100644
index 00000000..34e000ce
--- /dev/null
+++ b/src/OpenFeature.Hosting/Internal/EventHandlerDelegateWrapper.cs
@@ -0,0 +1,8 @@
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+namespace OpenFeature.Hosting.Internal;
+
+internal record EventHandlerDelegateWrapper(
+ ProviderEventTypes ProviderEventType,
+ EventHandlerDelegate EventHandlerDelegate);
diff --git a/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
new file mode 100644
index 00000000..4d915946
--- /dev/null
+++ b/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
@@ -0,0 +1,66 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace OpenFeature.Hosting.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);
+ }
+
+ var hooks = new List();
+ foreach (var hookName in options.HookNames)
+ {
+ var hook = _serviceProvider.GetRequiredKeyedService(hookName);
+ hooks.Add(hook);
+ }
+
+ _featureApi.AddHooks(hooks);
+
+ var handlers = _serviceProvider.GetServices();
+ foreach (var handler in handlers)
+ {
+ _featureApi.AddHandler(handler.ProviderEventType, handler.EventHandlerDelegate);
+ }
+ }
+
+ ///
+ 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.Hosting/MultiTarget/CallerArgumentExpressionAttribute.cs b/src/OpenFeature.Hosting/MultiTarget/CallerArgumentExpressionAttribute.cs
new file mode 100644
index 00000000..afbec6b0
--- /dev/null
+++ b/src/OpenFeature.Hosting/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.Hosting/MultiTarget/IsExternalInit.cs b/src/OpenFeature.Hosting/MultiTarget/IsExternalInit.cs
new file mode 100644
index 00000000..87714111
--- /dev/null
+++ b/src/OpenFeature.Hosting/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.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
index 1d54ff02..85131a0f 100644
--- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
+++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
@@ -1,7 +1,7 @@
-
+
- net8.0;net9.0
+ netstandard2.0;net8.0;net9.0;net462
OpenFeature
@@ -10,7 +10,12 @@
-
+
+
+
+
+
+
diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilder.cs b/src/OpenFeature.Hosting/OpenFeatureBuilder.cs
new file mode 100644
index 00000000..177a9fac
--- /dev/null
+++ b/src/OpenFeature.Hosting/OpenFeatureBuilder.cs
@@ -0,0 +1,60 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace OpenFeature.Hosting;
+
+///
+/// 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.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
index 80e760d9..52c66c42 100644
--- a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
+++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
@@ -1,6 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
-using OpenFeature.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using OpenFeature.Constant;
using OpenFeature.Hosting;
+using OpenFeature.Hosting.Internal;
+using OpenFeature.Model;
namespace OpenFeature;
@@ -9,6 +13,370 @@ namespace OpenFeature;
///
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 feature provider using a factory method without additional configuration options.
+ /// This method adds the feature provider as a transient service and sets it as the default provider within the application.
+ ///
+ /// The used to configure feature flags.
+ ///
+ /// A factory method that creates and returns a
+ /// instance based on the provided service provider.
+ ///
+ /// The updated instance with the default feature provider set and configured.
+ /// Thrown if the is null, as a valid builder is required to add and configure providers.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory)
+ => AddProvider(builder, implementationFactory, null);
+
+ ///
+ /// Adds a feature provider using a factory method to create the provider instance and optionally configures its settings.
+ /// This method adds the feature provider as a transient service and sets it as the default provider within the application.
+ ///
+ /// Type derived from used to configure the feature provider.
+ /// The used to configure feature flags.
+ ///
+ /// A factory method that creates and returns a
+ /// instance based on the provided service provider.
+ ///
+ /// An optional delegate to configure the provider-specific options.
+ /// The updated instance with the default feature provider set and configured.
+ /// Thrown if the is null, as a valid builder is required to add and configure providers.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory, Action? configureOptions)
+ where TOptions : OpenFeatureOptions
+ {
+ Guard.ThrowIfNull(builder);
+
+ builder.HasDefaultProvider = true;
+ builder.Services.PostConfigure(options => options.AddDefaultProviderName());
+ if (configureOptions != null)
+ {
+ builder.Services.Configure(configureOptions);
+ }
+
+ builder.Services.TryAddTransient(implementationFactory);
+ builder.AddClient();
+ return builder;
+ }
+
+ ///
+ /// Adds a feature provider for a specific domain using provided options and a configuration builder.
+ ///
+ /// Type derived from used to configure the feature provider.
+ /// The used to configure feature flags.
+ /// The unique name of the provider.
+ ///
+ /// A factory method that creates a feature provider instance.
+ /// It adds the provider as a transient service unless it is already added.
+ ///
+ /// An optional delegate to configure the provider-specific options.
+ /// The updated instance with the new feature provider configured.
+ ///
+ /// Thrown if either or is null or if the is empty.
+ ///
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory, Action? configureOptions)
+ where TOptions : OpenFeatureOptions
+ {
+ Guard.ThrowIfNull(builder);
+
+ builder.DomainBoundProviderRegistrationCount++;
+
+ builder.Services.PostConfigure(options => options.AddProviderName(domain));
+ if (configureOptions != null)
+ {
+ builder.Services.Configure(domain, configureOptions);
+ }
+
+ builder.Services.TryAddKeyedTransient(domain, (provider, key) =>
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+ return implementationFactory(provider, key.ToString()!);
+ });
+
+ builder.AddClient(domain);
+ return builder;
+ }
+
+ ///
+ /// Adds a feature provider for a specified domain using the default options.
+ /// This method configures a feature provider without custom options, delegating to the more generic AddProvider method.
+ ///
+ /// The used to configure feature flags.
+ /// The unique name of the provider.
+ ///
+ /// A factory method that creates a feature provider instance.
+ /// It adds the provider as a transient service unless it is already added.
+ ///
+ /// The updated instance with the new feature provider configured.
+ ///
+ /// Thrown if either or is null or if the is empty.
+ ///
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory)
+ => AddProvider(builder, domain, implementationFactory, configureOptions: null);
+
+ ///
+ /// 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;
+ }
+
+ ///
+ /// Adds a default to the based on the policy name options.
+ /// This method configures the dependency injection container to resolve the appropriate
+ /// depending on the policy name selected.
+ /// If no name is selected (i.e., null), it retrieves the default client.
+ ///
+ /// The instance.
+ /// The configured instance.
+ internal static OpenFeatureBuilder AddPolicyBasedClient(this OpenFeatureBuilder builder)
+ {
+ builder.Services.AddScoped(provider =>
+ {
+ var policy = provider.GetRequiredService>().Value;
+ var name = policy.DefaultNameSelector(provider);
+ if (name == null)
+ {
+ return provider.GetRequiredService();
+ }
+ return provider.GetRequiredKeyedService(name);
+ });
+
+ 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);
+
+ ///
+ /// Adds a feature hook to the service collection using a factory method. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// Optional factory for controlling how will be created in the DI container.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>(this OpenFeatureBuilder builder, Func? implementationFactory = null)
+ where THook : Hook
+ {
+ return builder.AddHook(typeof(THook).Name, implementationFactory);
+ }
+
+ ///
+ /// Adds a feature hook to the service collection. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// Instance of Hook to inject into the OpenFeature context.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>(this OpenFeatureBuilder builder, THook hook)
+ where THook : Hook
+ {
+ return builder.AddHook(typeof(THook).Name, hook);
+ }
+
+ ///
+ /// Adds a feature hook to the service collection with a specified name. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// The name of the that is being added.
+ /// Instance of Hook to inject into the OpenFeature context.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>(this OpenFeatureBuilder builder, string hookName, THook hook)
+ where THook : Hook
+ {
+ return builder.AddHook(hookName, _ => hook);
+ }
+
+ ///
+ /// Adds a feature hook to the service collection using a factory method and specified name. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// The name of the that is being added.
+ /// Optional factory for controlling how will be created in the DI container.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>
+ (this OpenFeatureBuilder builder, string hookName, Func? implementationFactory = null)
+ where THook : Hook
+ {
+ builder.Services.PostConfigure(options => options.AddHookName(hookName));
+
+ if (implementationFactory is not null)
+ {
+ builder.Services.TryAddKeyedSingleton(hookName, (serviceProvider, key) =>
+ {
+ return implementationFactory(serviceProvider);
+ });
+ }
+ else
+ {
+ builder.Services.TryAddKeyedSingleton(hookName);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Add a to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
+ ///
+ /// The instance.
+ /// The type to handle.
+ /// The handler which reacts to .
+ /// The instance.
+ public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, EventHandlerDelegate eventHandlerDelegate)
+ {
+ return AddHandler(builder, type, _ => eventHandlerDelegate);
+ }
+
+ ///
+ /// Add a to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
+ ///
+ /// The instance.
+ /// The type to handle.
+ /// The handler factory for creating a handler which reacts to .
+ /// The instance.
+ public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, Func implementationFactory)
+ {
+ builder.Services.AddSingleton((serviceProvider) =>
+ {
+ var handler = implementationFactory(serviceProvider);
+ return new EventHandlerDelegateWrapper(type, handler);
+ });
+
+ return builder;
+ }
+
///
/// Adds the to the OpenFeatureBuilder,
/// which manages the lifecycle of features within the application. It also allows
@@ -17,6 +385,7 @@ public static partial class OpenFeatureBuilderExtensions
/// The instance.
/// An optional action to configure .
/// The instance.
+ [Obsolete("Calling AddHostedFeatureLifecycle() is no longer necessary. OpenFeature will inject this automatically when you call AddOpenFeature().")]
public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null)
{
if (configureOptions is not null)
diff --git a/src/OpenFeature.Hosting/OpenFeatureOptions.cs b/src/OpenFeature.Hosting/OpenFeatureOptions.cs
new file mode 100644
index 00000000..9d3dd818
--- /dev/null
+++ b/src/OpenFeature.Hosting/OpenFeatureOptions.cs
@@ -0,0 +1,61 @@
+namespace OpenFeature.Hosting;
+
+///
+/// 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.
+ ///
+ protected internal 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.
+ protected internal void AddProviderName(string? name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ HasDefaultProvider = true;
+ }
+ else
+ {
+ lock (_providerNames)
+ {
+ _providerNames.Add(name!);
+ }
+ }
+ }
+
+ private readonly HashSet _hookNames = [];
+
+ internal IReadOnlyCollection HookNames => _hookNames;
+
+ internal void AddHookName(string name)
+ {
+ lock (_hookNames)
+ {
+ _hookNames.Add(name);
+ }
+ }
+}
diff --git a/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs
new file mode 100644
index 00000000..236dc62b
--- /dev/null
+++ b/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs
@@ -0,0 +1,62 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using OpenFeature.Hosting;
+using OpenFeature.Hosting.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);
+
+ builder.Services.AddHostedService();
+
+ // 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.AddPolicyBasedClient();
+
+ return services;
+ }
+}
diff --git a/src/OpenFeature.Hosting/PolicyNameOptions.cs b/src/OpenFeature.Hosting/PolicyNameOptions.cs
new file mode 100644
index 00000000..3dfa76f8
--- /dev/null
+++ b/src/OpenFeature.Hosting/PolicyNameOptions.cs
@@ -0,0 +1,12 @@
+namespace OpenFeature.Hosting;
+
+///
+/// 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.Hosting/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs
new file mode 100644
index 00000000..d63009d6
--- /dev/null
+++ b/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs
@@ -0,0 +1,126 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.Hosting.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 a factory for flags.
+ ///
+ /// The instance to configure.
+ ///
+ /// A factory function to provide an of flags.
+ /// If null, an empty provider will be created.
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Func?> flagsFactory)
+ => builder.AddProvider(provider =>
+ {
+ var flags = flagsFactory(provider);
+ if (flags == null)
+ {
+ return new InMemoryProvider();
+ }
+
+ return new InMemoryProvider(flags);
+ });
+
+ ///
+ /// Adds an in-memory feature provider to the with a domain and factory for flags.
+ ///
+ /// The instance to configure.
+ /// The unique domain of the provider.
+ ///
+ /// A factory function to provide an of flags.
+ /// If null, an empty provider will be created.
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Func?> flagsFactory)
+ => builder.AddInMemoryProvider(domain, (provider, _) => flagsFactory(provider));
+
+ ///
+ /// Adds an in-memory feature provider to the with a domain and contextual flag factory.
+ /// If null, an empty provider will be created.
+ ///
+ /// The instance to configure.
+ /// The unique domain of the provider.
+ ///
+ /// A factory function to provide an of flags based on service provider and domain.
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Func?> flagsFactory)
+ => builder.AddProvider(domain, (provider, key) =>
+ {
+ var flags = flagsFactory(provider, key);
+ if (flags == null)
+ {
+ return new InMemoryProvider();
+ }
+
+ return new InMemoryProvider(flags);
+ });
+
+ ///
+ /// 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 null, an empty provider will be created.
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null)
+ => builder.AddProvider(CreateProvider, options => ConfigureFlags(options, 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 null, an empty provider will be created.
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null)
+ => builder.AddProvider(domain, CreateProvider, options => ConfigureFlags(options, configure));
+
+ private static FeatureProvider CreateProvider(IServiceProvider provider, string domain)
+ {
+ var options = provider.GetRequiredService>().Get(domain);
+ if (options.Flags == null)
+ {
+ return new InMemoryProvider();
+ }
+
+ return new InMemoryProvider(options.Flags);
+ }
+
+ private static FeatureProvider CreateProvider(IServiceProvider provider)
+ {
+ var options = provider.GetRequiredService>().Value;
+ if (options.Flags == null)
+ {
+ return new InMemoryProvider();
+ }
+
+ return new InMemoryProvider(options.Flags);
+ }
+
+ private static void ConfigureFlags(InMemoryProviderOptions options, Action>? configure)
+ {
+ if (configure != null)
+ {
+ options.Flags = new Dictionary();
+ configure.Invoke(options.Flags);
+ }
+ }
+}
diff --git a/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs b/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs
new file mode 100644
index 00000000..3e7431ee
--- /dev/null
+++ b/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs
@@ -0,0 +1,19 @@
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.Hosting.Providers.Memory;
+
+///
+/// Options for configuring the in-memory feature flag provider.
+///
+public class InMemoryProviderOptions : OpenFeatureOptions
+{
+ ///
+ /// Gets or sets the feature flags to be used by the in-memory provider.
+ ///
+ ///
+ /// This property allows you to specify a dictionary of flags where the key is the flag name
+ /// and the value is the corresponding instance.
+ /// If no flags are provided, the in-memory provider will start with an empty set of flags.
+ ///
+ public IDictionary? Flags { get; set; }
+}
diff --git a/test/OpenFeature.Hosting.Tests/GuardTests.cs b/test/OpenFeature.Hosting.Tests/GuardTests.cs
new file mode 100644
index 00000000..13b8883d
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/GuardTests.cs
@@ -0,0 +1,30 @@
+namespace OpenFeature.Hosting.Tests;
+
+public class GuardTests
+{
+ [Fact]
+ public void ThrowIfNull_WithNullArgument_ThrowsArgumentNullException()
+ {
+ // Arrange
+ object? argument = null;
+
+ // Act
+ var exception = Assert.Throws(() => Guard.ThrowIfNull(argument));
+
+ // Assert
+ Assert.Equal("argument", exception.ParamName);
+ }
+
+ [Fact]
+ public void ThrowIfNull_WithNotNullArgument_DoesNotThrowArgumentNullException()
+ {
+ // Arrange
+ object? argument = "Test argument";
+
+ // Act
+ var ex = Record.Exception(() => Guard.ThrowIfNull(argument));
+
+ // Assert
+ Assert.Null(ex);
+ }
+}
diff --git a/test/OpenFeature.Hosting.Tests/Internal/FeatureLifecycleManagerTests.cs b/test/OpenFeature.Hosting.Tests/Internal/FeatureLifecycleManagerTests.cs
new file mode 100644
index 00000000..2d379fc4
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/Internal/FeatureLifecycleManagerTests.cs
@@ -0,0 +1,203 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Testing;
+using OpenFeature.Constant;
+using OpenFeature.Hosting.Internal;
+
+namespace OpenFeature.Hosting.Tests.Internal;
+
+public class FeatureLifecycleManagerTests : IAsyncLifetime
+{
+ [Fact]
+ public async Task EnsureInitializedAsync_SetsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = new NoOpFeatureProvider();
+ services.AddOptions().Configure(options =>
+ {
+ options.AddProviderName(null);
+ });
+ services.AddSingleton(provider);
+
+ var api = Api.Instance;
+
+ // Act
+ using var serviceProvider = services.BuildServiceProvider();
+ var lifecycleManager = new FeatureLifecycleManager(api, serviceProvider, NullLogger.Instance);
+ await lifecycleManager.EnsureInitializedAsync();
+
+ // Assert
+ var actualProvider = api.GetProvider();
+ Assert.Equal(provider, actualProvider);
+ }
+
+ [Fact]
+ public async Task EnsureInitializedAsync_SetsMultipleProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider1 = new NoOpFeatureProvider();
+ var provider2 = new NoOpFeatureProvider();
+ services.AddOptions().Configure(options =>
+ {
+ options.AddProviderName("provider1");
+ options.AddProviderName("provider2");
+ });
+ services.AddKeyedSingleton("provider1", provider1);
+ services.AddKeyedSingleton("provider2", provider2);
+
+ var api = Api.Instance;
+
+ // Act
+ using var serviceProvider = services.BuildServiceProvider();
+ var lifecycleManager = new FeatureLifecycleManager(api, serviceProvider, NullLogger.Instance);
+ await lifecycleManager.EnsureInitializedAsync();
+
+ // Assert
+ Assert.Equal(provider1, api.GetProvider("provider1"));
+ Assert.Equal(provider2, api.GetProvider("provider2"));
+ }
+
+ [Fact]
+ public async Task EnsureInitializedAsync_AddsHooks()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = new NoOpFeatureProvider();
+ var hook = new NoOpHook();
+ services.AddOptions().Configure(options =>
+ {
+ options.AddProviderName(null);
+ options.AddHookName("TestHook");
+ });
+ services.AddSingleton(provider);
+ services.AddKeyedSingleton("TestHook", hook);
+
+ var api = Api.Instance;
+
+ // Act
+ using var serviceProvider = services.BuildServiceProvider();
+ var lifecycleManager = new FeatureLifecycleManager(api, serviceProvider, NullLogger.Instance);
+ await lifecycleManager.EnsureInitializedAsync();
+
+ // Assert
+ var actualHooks = api.GetHooks();
+ Assert.Single(actualHooks);
+ Assert.Contains(hook, actualHooks);
+ }
+
+ [Fact]
+ public async Task EnsureInitializedAsync_AddHandlers()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = new NoOpFeatureProvider();
+ services.AddOptions().Configure(options =>
+ {
+ options.AddProviderName(null);
+ });
+ services.AddSingleton(provider);
+
+ bool hookExecuted = false;
+ services.AddSingleton(new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, (p) => { hookExecuted = true; }));
+
+ var api = Api.Instance;
+
+ // Act
+ using var serviceProvider = services.BuildServiceProvider();
+ var lifecycleManager = new FeatureLifecycleManager(api, serviceProvider, NullLogger.Instance);
+ await lifecycleManager.EnsureInitializedAsync();
+
+ // Assert
+ Assert.True(hookExecuted);
+ }
+
+ [Fact]
+ public async Task ShutdownAsync_ResetsApi()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = new NoOpFeatureProvider();
+
+ var api = Api.Instance;
+ await api.SetProviderAsync(provider);
+ api.AddHooks(new NoOpHook());
+
+ // Act
+ using var serviceProvider = services.BuildServiceProvider();
+ var lifecycleManager = new FeatureLifecycleManager(api, serviceProvider, NullLogger.Instance);
+ await lifecycleManager.ShutdownAsync();
+
+ // Assert
+ var actualProvider = api.GetProvider();
+ Assert.NotEqual(provider, actualProvider); // Default provider should be set after shutdown
+ Assert.Empty(api.GetHooks()); // Hooks should be cleared
+ }
+
+ [Fact]
+ public async Task EnsureInitializedAsync_LogStartingInitialization()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = new NoOpFeatureProvider();
+ services.AddOptions().Configure(options =>
+ {
+ options.AddProviderName(null);
+ });
+ services.AddSingleton(provider);
+
+ var api = Api.Instance;
+ var logger = new FakeLogger();
+
+ // Act
+ using var serviceProvider = services.BuildServiceProvider();
+ var lifecycleManager = new FeatureLifecycleManager(api, serviceProvider, logger);
+ await lifecycleManager.EnsureInitializedAsync();
+
+ // Assert
+ var log = logger.LatestRecord;
+ Assert.Equal(200, log.Id);
+ Assert.Equal("Starting initialization of the feature provider", log.Message);
+ Assert.Equal(LogLevel.Information, log.Level);
+ }
+
+ [Fact]
+ public async Task ShutdownAsync_LogShuttingDown()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var provider = new NoOpFeatureProvider();
+ services.AddOptions().Configure(options =>
+ {
+ options.AddProviderName(null);
+ });
+ services.AddSingleton(provider);
+
+ var api = Api.Instance;
+ var logger = new FakeLogger();
+
+ // Act
+ using var serviceProvider = services.BuildServiceProvider();
+ var lifecycleManager = new FeatureLifecycleManager(api, serviceProvider, logger);
+ await lifecycleManager.ShutdownAsync();
+
+ // Assert
+ var log = logger.LatestRecord;
+ Assert.Equal(200, log.Id);
+ Assert.Equal("Shutting down the feature provider", log.Message);
+ Assert.Equal(LogLevel.Information, log.Level);
+ }
+
+ public async Task InitializeAsync()
+ {
+ await Api.Instance.ShutdownAsync();
+ }
+
+ // Make sure the singleton is cleared between tests
+ public async Task DisposeAsync()
+ {
+ await Api.Instance.ShutdownAsync().ConfigureAwait(false);
+ }
+}
diff --git a/test/OpenFeature.Hosting.Tests/NoOpFeatureProvider.cs b/test/OpenFeature.Hosting.Tests/NoOpFeatureProvider.cs
new file mode 100644
index 00000000..a19a78b3
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/NoOpFeatureProvider.cs
@@ -0,0 +1,52 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.Hosting.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.Hosting.Tests/NoOpHook.cs b/test/OpenFeature.Hosting.Tests/NoOpHook.cs
new file mode 100644
index 00000000..a0085f3b
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/NoOpHook.cs
@@ -0,0 +1,26 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.Hosting.Tests;
+
+internal class NoOpHook : Hook
+{
+ public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ return base.BeforeAsync(context, hints, cancellationToken);
+ }
+
+ public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ return base.AfterAsync(context, details, hints, cancellationToken);
+ }
+
+ public override ValueTask FinallyAsync(HookContext context, FlagEvaluationDetails evaluationDetails, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken);
+ }
+
+ public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ return base.ErrorAsync(context, error, hints, cancellationToken);
+ }
+}
diff --git a/test/OpenFeature.Hosting.Tests/NoOpProvider.cs b/test/OpenFeature.Hosting.Tests/NoOpProvider.cs
new file mode 100644
index 00000000..423cd361
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/NoOpProvider.cs
@@ -0,0 +1,8 @@
+namespace OpenFeature.Hosting.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.Hosting.Tests/OpenFeature.Hosting.Tests.csproj b/test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj
new file mode 100644
index 00000000..ae8707a8
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net8.0;net9.0
+ $(TargetFrameworks);net462
+ OpenFeature.Hosting.Tests
+
+
+
+
+
+ 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.Hosting.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderExtensionsTests.cs
new file mode 100644
index 00000000..1a284c91
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderExtensionsTests.cs
@@ -0,0 +1,562 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using OpenFeature.Hosting.Internal;
+using OpenFeature.Model;
+
+namespace OpenFeature.Hosting.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 featureBuilder = useServiceProviderDelegate ?
+ _systemUnderTest.AddContext(_ => { }) :
+ _systemUnderTest.AddContext((_, _) => { });
+
+ // Assert
+ Assert.Equal(_systemUnderTest, featureBuilder);
+ Assert.True(_systemUnderTest.IsContextConfigured, "The context should be configured.");
+ Assert.Single(_services, serviceDescriptor =>
+ serviceDescriptor.ServiceType == typeof(EvaluationContext) &&
+ serviceDescriptor.Lifetime == ServiceLifetime.Transient);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate)
+ {
+ // Arrange
+ var delegateCalled = false;
+
+ _ = useServiceProviderDelegate ?
+ _systemUnderTest.AddContext(_ => delegateCalled = true) :
+ _systemUnderTest.AddContext((_, _) => delegateCalled = true);
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var context = serviceProvider.GetService();
+
+ // Assert
+ Assert.True(_systemUnderTest.IsContextConfigured, "The context should be configured.");
+ Assert.NotNull(context);
+ Assert.True(delegateCalled, "The delegate should be invoked.");
+ }
+
+#if NET8_0_OR_GREATER
+ [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+ [Theory]
+ [InlineData(1, true, 0)]
+ [InlineData(2, false, 1)]
+ [InlineData(3, true, 0)]
+ [InlineData(4, false, 1)]
+ public void AddProvider_ShouldAddProviderToCollection(int providerRegistrationType, bool expectsDefaultProvider, int expectsDomainBoundProvider)
+ {
+ // Act
+ var featureBuilder = providerRegistrationType switch
+ {
+ 1 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()),
+ 2 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider()),
+ 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { }),
+ 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }),
+ _ => throw new InvalidOperationException("Invalid mode.")
+ };
+
+ // Assert
+ Assert.False(_systemUnderTest.IsContextConfigured, "The context should not be configured.");
+ Assert.Equal(expectsDefaultProvider, _systemUnderTest.HasDefaultProvider);
+ Assert.False(_systemUnderTest.IsPolicyConfigured, "The policy should not be configured.");
+ Assert.Equal(expectsDomainBoundProvider, _systemUnderTest.DomainBoundProviderRegistrationCount);
+ Assert.Equal(_systemUnderTest, featureBuilder);
+ Assert.Single(_services, serviceDescriptor =>
+ serviceDescriptor.ServiceType == typeof(FeatureProvider) &&
+ serviceDescriptor.Lifetime == ServiceLifetime.Transient);
+ }
+
+ class TestOptions : OpenFeatureOptions { }
+
+#if NET8_0_OR_GREATER
+ [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
+#endif
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ [InlineData(4)]
+ public void AddProvider_ShouldResolveCorrectProvider(int providerRegistrationType)
+ {
+ // Arrange
+ _ = providerRegistrationType switch
+ {
+ 1 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()),
+ 2 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider()),
+ 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { }),
+ 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }),
+ _ => throw new InvalidOperationException("Invalid mode.")
+ };
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var provider = providerRegistrationType switch
+ {
+ 1 or 3 => serviceProvider.GetService(),
+ 2 or 4 => serviceProvider.GetKeyedService("test"),
+ _ => throw new InvalidOperationException("Invalid mode.")
+ };
+
+ // Assert
+ Assert.NotNull(provider);
+ Assert.IsType(provider);
+ }
+
+ [Theory]
+ [InlineData(1, true, 1)]
+ [InlineData(2, true, 1)]
+ [InlineData(3, false, 2)]
+ [InlineData(4, true, 1)]
+ [InlineData(5, true, 1)]
+ [InlineData(6, false, 2)]
+ [InlineData(7, true, 2)]
+ [InlineData(8, true, 2)]
+ public void AddProvider_VerifiesDefaultAndDomainBoundProvidersBasedOnConfiguration(int providerRegistrationType, bool expectsDefaultProvider, int expectsDomainBoundProvider)
+ {
+ // Act
+ var featureBuilder = providerRegistrationType switch
+ {
+ 1 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider())
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider()),
+ 2 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider())
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider()),
+ 3 => _systemUnderTest
+ .AddProvider("test1", (_, _) => new NoOpFeatureProvider())
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider()),
+ 4 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider()),
+ 5 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider()),
+ 6 => _systemUnderTest
+ .AddProvider("test1", (_, _) => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider()),
+ 7 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider())
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider())
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider()),
+ 8 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider(), o => { }),
+ _ => throw new InvalidOperationException("Invalid mode.")
+ };
+
+ // Assert
+ Assert.False(_systemUnderTest.IsContextConfigured, "The context should not be configured.");
+ Assert.Equal(expectsDefaultProvider, _systemUnderTest.HasDefaultProvider);
+ Assert.False(_systemUnderTest.IsPolicyConfigured, "The policy should not be configured.");
+ Assert.Equal(expectsDomainBoundProvider, _systemUnderTest.DomainBoundProviderRegistrationCount);
+ Assert.Equal(_systemUnderTest, featureBuilder);
+ }
+
+ [Theory]
+ [InlineData(1, null)]
+ [InlineData(2, "test")]
+ [InlineData(3, "test2")]
+ [InlineData(4, "test")]
+ [InlineData(5, null)]
+ [InlineData(6, "test1")]
+ [InlineData(7, "test2")]
+ [InlineData(8, null)]
+ public void AddProvider_ConfiguresPolicyNameAcrossMultipleProviderSetups(int providerRegistrationType, string? policyName)
+ {
+ // Arrange
+ var featureBuilder = providerRegistrationType switch
+ {
+ 1 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider())
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider())
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ 2 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider())
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider())
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ 3 => _systemUnderTest
+ .AddProvider("test1", (_, _) => new NoOpFeatureProvider())
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider())
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ 4 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider())
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ 5 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider())
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ 6 => _systemUnderTest
+ .AddProvider("test1", (_, _) => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider())
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ 7 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider())
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider())
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider())
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ 8 => _systemUnderTest
+ .AddProvider(_ => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { })
+ .AddProvider("test2", (_, _) => new NoOpFeatureProvider(), o => { })
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName),
+ _ => throw new InvalidOperationException("Invalid mode.")
+ };
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var policy = serviceProvider.GetRequiredService>().Value;
+ var name = policy.DefaultNameSelector(serviceProvider);
+ var provider = name == null ?
+ serviceProvider.GetService() :
+ serviceProvider.GetRequiredKeyedService(name);
+
+ // Assert
+ Assert.True(featureBuilder.IsPolicyConfigured, "The policy should be configured.");
+ Assert.NotNull(provider);
+ Assert.IsType(provider);
+ }
+
+ [Fact]
+ public void AddProvider_WithNullKey_ThrowsArgumentNullException()
+ {
+ // Arrange & Act
+ _systemUnderTest.AddProvider(null!, (sp, domain) => new NoOpFeatureProvider());
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ var ex = Assert.Throws(() => serviceProvider.GetKeyedService(null));
+
+ Assert.Equal("key", ex.ParamName);
+ }
+
+ [Fact]
+ public void AddHook_AddsHookAsKeyedService()
+ {
+ // Arrange
+ _systemUnderTest.AddHook();
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var hook = serviceProvider.GetKeyedService("NoOpHook");
+
+ // Assert
+ Assert.NotNull(hook);
+ }
+
+ [Fact]
+ public void AddHook_AddsHookNameToOpenFeatureOptions()
+ {
+ // Arrange
+ _systemUnderTest.AddHook(sp => new NoOpHook());
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var options = serviceProvider.GetRequiredService>();
+
+ // Assert
+ Assert.Contains(options.Value.HookNames, t => t == "NoOpHook");
+ }
+
+ [Fact]
+ public void AddHook_WithSpecifiedNameToOpenFeatureOptions()
+ {
+ // Arrange
+ _systemUnderTest.AddHook("my-custom-name");
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var hook = serviceProvider.GetKeyedService("my-custom-name");
+
+ // Assert
+ Assert.NotNull(hook);
+ }
+
+ [Fact]
+ public void AddHook_WithSpecifiedNameAndImplementationFactory_AsKeyedService()
+ {
+ // Arrange
+ _systemUnderTest.AddHook("my-custom-name", (serviceProvider) => new NoOpHook());
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var hook = serviceProvider.GetKeyedService("my-custom-name");
+
+ // Assert
+ Assert.NotNull(hook);
+ }
+
+ [Fact]
+ public void AddHook_WithInstance_AddsHookAsKeyedService()
+ {
+ // Arrange
+ var expectedHook = new NoOpHook();
+ _systemUnderTest.AddHook(expectedHook);
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var actualHook = serviceProvider.GetKeyedService("NoOpHook");
+
+ // Assert
+ Assert.NotNull(actualHook);
+ Assert.Equal(expectedHook, actualHook);
+ }
+
+ [Fact]
+ public void AddHook_WithSpecifiedNameAndInstance_AddsHookAsKeyedService()
+ {
+ // Arrange
+ var expectedHook = new NoOpHook();
+ _systemUnderTest.AddHook("custom-hook", expectedHook);
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var actualHook = serviceProvider.GetKeyedService("custom-hook");
+
+ // Assert
+ Assert.NotNull(actualHook);
+ Assert.Equal(expectedHook, actualHook);
+ }
+
+ [Fact]
+ public void AddHandler_AddsEventHandlerDelegateWrapperAsKeyedService()
+ {
+ // Arrange
+ EventHandlerDelegate eventHandler = (eventDetails) => { };
+ _systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, eventHandler);
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var handler = serviceProvider.GetService();
+
+ // Assert
+ Assert.NotNull(handler);
+ Assert.Equal(eventHandler, handler.EventHandlerDelegate);
+ }
+
+ [Fact]
+ public void AddHandlerTwice_MultipleEventHandlerDelegateWrappersAsKeyedServices()
+ {
+ // Arrange
+ EventHandlerDelegate eventHandler1 = (eventDetails) => { };
+ EventHandlerDelegate eventHandler2 = (eventDetails) => { };
+ _systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, eventHandler1);
+ _systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, eventHandler2);
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var handler = serviceProvider.GetServices();
+
+ // Assert
+ Assert.NotEmpty(handler);
+ Assert.Equal(eventHandler1, handler.ElementAt(0).EventHandlerDelegate);
+ Assert.Equal(eventHandler2, handler.ElementAt(1).EventHandlerDelegate);
+ }
+
+ [Fact]
+ public void AddHandler_WithImplementationFactory_AddsEventHandlerDelegateWrapperAsKeyedService()
+ {
+ // Arrange
+ EventHandlerDelegate eventHandler = (eventDetails) => { };
+ _systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, _ => eventHandler);
+
+ var serviceProvider = _services.BuildServiceProvider();
+
+ // Act
+ var handler = serviceProvider.GetService();
+
+ // Assert
+ Assert.NotNull(handler);
+ Assert.Equal(eventHandler, handler.EventHandlerDelegate);
+ }
+
+ [Fact]
+ public void AddClient_AddsFeatureClient()
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ // Act
+ _systemUnderTest.AddClient();
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ var client = serviceProvider.GetService();
+
+ Assert.NotNull(client);
+ }
+
+ [Fact]
+ public void AddClient_WithContext_AddsFeatureClient()
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ _systemUnderTest
+ .AddContext((a) => a.Set("region", "euw"))
+ .AddProvider(_systemUnderTest => new NoOpFeatureProvider());
+
+ // Act
+ _systemUnderTest.AddClient();
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ var client = serviceProvider.GetService();
+
+ Assert.NotNull(client);
+
+ var context = client.GetContext();
+ var region = context.GetValue("region");
+ Assert.Equal("euw", region.AsString);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void AddClient_WithInvalidName_AddsFeatureClient(string? name)
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ // Act
+ _systemUnderTest.AddClient(name);
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ var client = serviceProvider.GetService();
+ Assert.NotNull(client);
+
+ var keyedClients = serviceProvider.GetKeyedServices(name);
+ Assert.Empty(keyedClients);
+ }
+
+ [Fact]
+ public void AddClient_WithNullName_AddsFeatureClient()
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ // Act
+ _systemUnderTest.AddClient(null);
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ var client = serviceProvider.GetService();
+ Assert.NotNull(client);
+ }
+
+ [Fact]
+ public void AddClient_WithName_AddsFeatureClient()
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ // Act
+ _systemUnderTest.AddClient("client-name");
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ var client = serviceProvider.GetKeyedService("client-name");
+
+ Assert.NotNull(client);
+ }
+
+ [Fact]
+ public void AddClient_WithNameAndContext_AddsFeatureClient()
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ _systemUnderTest
+ .AddContext((a) => a.Set("region", "euw"))
+ .AddProvider(_systemUnderTest => new NoOpFeatureProvider());
+
+ // Act
+ _systemUnderTest.AddClient("client-name");
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ var client = serviceProvider.GetKeyedService("client-name");
+
+ Assert.NotNull(client);
+
+ var context = client.GetContext();
+ var region = context.GetValue("region");
+ Assert.Equal("euw", region.AsString);
+ }
+
+ [Fact]
+ public void AddPolicyBasedClient_AddsScopedFeatureClient()
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ _services.AddOptions()
+ .Configure(options => options.DefaultNameSelector = _ => "default-name");
+
+ _systemUnderTest.AddProvider("default-name", (_, key) => new NoOpFeatureProvider());
+
+ // Act
+ _systemUnderTest.AddPolicyBasedClient();
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ using var scope = serviceProvider.CreateScope();
+ var client = scope.ServiceProvider.GetService();
+ Assert.NotNull(client);
+ }
+
+ [Fact(Skip = "Bug due to https://github.com/open-feature/dotnet-sdk/issues/543")]
+ public void AddPolicyBasedClient_WithNoDefaultName_AddsScopedFeatureClient()
+ {
+ // Arrange
+ _services.AddSingleton(sp => Api.Instance);
+
+ _services.AddOptions()
+ .Configure(options => options.DefaultNameSelector = sp => null);
+
+ _systemUnderTest.AddProvider("default", (_, key) => new NoOpFeatureProvider());
+
+ // Act
+ _systemUnderTest.AddPolicyBasedClient();
+
+ // Assert
+ using var serviceProvider = _services.BuildServiceProvider();
+ using var scope = serviceProvider.CreateScope();
+ var client = scope.ServiceProvider.GetService();
+ Assert.NotNull(client);
+ }
+}
diff --git a/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderTests.cs b/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderTests.cs
new file mode 100644
index 00000000..6c4ea993
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderTests.cs
@@ -0,0 +1,93 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace OpenFeature.Hosting.Tests;
+
+public class OpenFeatureBuilderTests
+{
+ [Fact]
+ public void Validate_DoesNotThrowException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ var ex = Record.Exception(builder.Validate);
+
+ // Assert
+ Assert.Null(ex);
+ }
+
+ [Fact]
+ public void Validate_WithPolicySet_DoesNotThrowException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services)
+ {
+ IsPolicyConfigured = true
+ };
+
+ // Act
+ var ex = Record.Exception(builder.Validate);
+
+ // Assert
+ Assert.Null(ex);
+ }
+
+ [Fact]
+ public void Validate_WithMultipleDomainProvidersRegistered_ThrowInvalidOperationException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services)
+ {
+ IsPolicyConfigured = false,
+ DomainBoundProviderRegistrationCount = 2
+ };
+
+ // Act
+ var ex = Assert.Throws(builder.Validate);
+
+ // Assert
+ Assert.Equal("Multiple providers have been registered, but no policy has been configured.", ex.Message);
+ }
+
+ [Fact]
+ public void Validate_WithDefaultAndDomainProvidersRegistered_ThrowInvalidOperationException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services)
+ {
+ IsPolicyConfigured = false,
+ DomainBoundProviderRegistrationCount = 1,
+ HasDefaultProvider = true
+ };
+
+ // Act
+ var ex = Assert.Throws(builder.Validate);
+
+ // Assert
+ Assert.Equal("A default provider and an additional provider have been registered without a policy configuration.", ex.Message);
+ }
+
+ [Fact]
+ public void Validate_WithNoDefaultProviderRegistered_DoesNotThrow()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services)
+ {
+ IsPolicyConfigured = false,
+ DomainBoundProviderRegistrationCount = 1,
+ HasDefaultProvider = false
+ };
+
+ // Act
+ var ex = Record.Exception(builder.Validate);
+
+ // Assert
+ Assert.Null(ex);
+ }
+}
diff --git a/test/OpenFeature.Hosting.Tests/OpenFeatureOptionsTests.cs b/test/OpenFeature.Hosting.Tests/OpenFeatureOptionsTests.cs
new file mode 100644
index 00000000..d39d4059
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/OpenFeatureOptionsTests.cs
@@ -0,0 +1,73 @@
+namespace OpenFeature.Hosting.Tests;
+
+public class OpenFeatureOptionsTests
+{
+ [Fact]
+ public void AddProviderName_DoesNotSetHasDefaultProvider()
+ {
+ // Arrange
+ var options = new OpenFeatureOptions();
+
+ // Act
+ options.AddProviderName("TestProvider");
+
+ // Assert
+ Assert.False(options.HasDefaultProvider);
+ }
+
+ [Fact]
+ public void AddProviderName_WithNullName_SetsHasDefaultProvider()
+ {
+ // Arrange
+ var options = new OpenFeatureOptions();
+
+ // Act
+ options.AddProviderName(null);
+
+ // Assert
+ Assert.True(options.HasDefaultProvider);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void AddProviderName_WithEmptyName_SetsHasDefaultProvider(string name)
+ {
+ // Arrange
+ var options = new OpenFeatureOptions();
+
+ // Act
+ options.AddProviderName(name);
+
+ // Assert
+ Assert.True(options.HasDefaultProvider);
+ }
+
+ [Fact]
+ public void AddProviderName_WithSameName_OnlyRegistersNameOnce()
+ {
+ // Arrange
+ var options = new OpenFeatureOptions();
+
+ // Act
+ options.AddProviderName("test-provider");
+ options.AddProviderName("test-provider");
+ options.AddProviderName("test-provider");
+
+ // Assert
+ Assert.Single(options.ProviderNames);
+ }
+
+ [Fact]
+ public void AddHookName_RegistersHookName()
+ {
+ // Arrange
+ var options = new OpenFeatureOptions();
+
+ // Act
+ options.AddHookName("test-hook");
+
+ // Assert
+ Assert.Single(options.HookNames);
+ }
+}
diff --git a/test/OpenFeature.Hosting.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.Hosting.Tests/OpenFeatureServiceCollectionExtensionsTests.cs
new file mode 100644
index 00000000..dc3cc934
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/OpenFeatureServiceCollectionExtensionsTests.cs
@@ -0,0 +1,95 @@
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+
+namespace OpenFeature.Hosting.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);
+
+ Assert.Single(_systemUnderTest, s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton);
+ Assert.Single(_systemUnderTest, s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton);
+ Assert.Single(_systemUnderTest, 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());
+ }
+
+ [Fact]
+ public void AddOpenFeature_WithDefaultProvider()
+ {
+ // Act
+ _systemUnderTest.AddOpenFeature(builder =>
+ {
+ builder.AddProvider(_ => new NoOpFeatureProvider());
+ });
+
+ // Assert
+ using var serviceProvider = _systemUnderTest.BuildServiceProvider();
+ var featureClient = serviceProvider.GetRequiredService();
+ Assert.NotNull(featureClient);
+ }
+
+ [Fact]
+ public void AddOpenFeature_WithNamedDefaultProvider()
+ {
+ // Act
+ _systemUnderTest.AddOpenFeature(builder =>
+ {
+ builder.AddProvider("no-opprovider", (_, key) => new NoOpFeatureProvider());
+ });
+
+ // Assert
+ using var serviceProvider = _systemUnderTest.BuildServiceProvider();
+ var featureClient = serviceProvider.GetRequiredService();
+ Assert.NotNull(featureClient);
+ }
+
+ [Fact]
+ public void AddOpenFeature_WithNamedDefaultProvider_InvokesAddPolicyName()
+ {
+ // Arrange
+ var provider1 = new NoOpFeatureProvider();
+ var provider2 = new NoOpFeatureProvider();
+
+ // Act
+ _systemUnderTest.AddOpenFeature(builder =>
+ {
+ builder
+ .AddPolicyName(ss =>
+ {
+ ss.DefaultNameSelector = (sp) => "no-opprovider";
+ })
+ .AddProvider("no-opprovider", (_, key) => provider1)
+ .AddProvider("no-opprovider-2", (_, key) => provider2);
+ });
+
+ // Assert
+ using var serviceProvider = _systemUnderTest.BuildServiceProvider();
+ var client = serviceProvider.GetKeyedService("no-opprovider");
+ Assert.NotNull(client);
+
+ var otherClient = serviceProvider.GetService();
+ Assert.NotNull(otherClient);
+ }
+}
diff --git a/test/OpenFeature.Hosting.Tests/Providers/Memory/FeatureBuilderExtensionsTests.cs b/test/OpenFeature.Hosting.Tests/Providers/Memory/FeatureBuilderExtensionsTests.cs
new file mode 100644
index 00000000..b36dc82d
--- /dev/null
+++ b/test/OpenFeature.Hosting.Tests/Providers/Memory/FeatureBuilderExtensionsTests.cs
@@ -0,0 +1,257 @@
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature.Hosting.Providers.Memory;
+using OpenFeature.Model;
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.Hosting.Tests.Providers.Memory;
+
+public class FeatureBuilderExtensionsTests
+{
+ [Fact]
+ public void AddInMemoryProvider_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider();
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetService();
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+ }
+
+ [Fact]
+ public async Task AddInMemoryProvider_WithFlags_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ Func enableForAlphaGroup = ctx => ctx.GetValue("group").AsString == "alpha" ? "on" : "off";
+ var flags = new Dictionary
+ {
+ { "feature1", new Flag(new Dictionary { { "on", true }, { "off", false } }, "on") },
+ { "feature2", new Flag(new Dictionary { { "on", true }, { "off", false } }, "on", enableForAlphaGroup) }
+ };
+
+ // Act
+ builder.AddInMemoryProvider((sp) => flags);
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetService();
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+
+ var result = await featureProvider.ResolveBooleanValueAsync("feature1", false);
+ Assert.True(result.Value);
+ }
+
+ [Fact]
+ public void AddInMemoryProvider_WithNullFlagsFactory_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider((sp) => null);
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetService();
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+ }
+
+ [Fact]
+ public void AddInMemoryProvider_WithNullConfigure_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider((Action>?)null);
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetService();
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+ }
+
+ [Fact]
+ public void AddInMemoryProvider_WithDomain_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider("domain");
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetKeyedService("domain");
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+ }
+
+ [Fact]
+ public async Task AddInMemoryProvider_WithDomainAndFlags_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ Func enableForAlphaGroup = ctx => ctx.GetValue("group").AsString == "alpha" ? "on" : "off";
+ var flags = new Dictionary
+ {
+ { "feature1", new Flag(new Dictionary { { "on", true }, { "off", false } }, "on") },
+ { "feature2", new Flag(new Dictionary { { "on", true }, { "off", false } }, "off", enableForAlphaGroup) }
+ };
+
+ // Act
+ builder.AddInMemoryProvider("domain", (sp) => flags);
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetKeyedService("domain");
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+
+ var context = EvaluationContext.Builder().Set("group", "alpha").Build();
+ var result = await featureProvider.ResolveBooleanValueAsync("feature2", false, context);
+ Assert.True(result.Value);
+ }
+
+ [Fact]
+ public void AddInMemoryProvider_WithDomainAndNullFlagsFactory_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider("domain", (sp) => null);
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetKeyedService("domain");
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+ }
+
+ [Fact]
+ public async Task AddInMemoryProvider_WithOptions_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddOptions()
+ .Configure((opts) =>
+ {
+ opts.Flags = new Dictionary
+ {
+ { "new-feature", new Flag(new Dictionary { { "on", true }, { "off", false } }, "off") },
+ };
+ });
+
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider();
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetService();
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+
+ var result = await featureProvider.ResolveBooleanValueAsync("new-feature", true);
+ Assert.False(result.Value);
+ }
+
+ [Fact]
+ public async Task AddInMemoryProvider_WithDomainAndOptions_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddOptions("domain-name")
+ .Configure((opts) =>
+ {
+ opts.Flags = new Dictionary
+ {
+ { "new-feature", new Flag(new Dictionary { { "on", true }, { "off", false } }, "off") },
+ };
+ });
+
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider("domain-name");
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetKeyedService("domain-name");
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+
+ var result = await featureProvider.ResolveBooleanValueAsync("new-feature", true);
+ Assert.False(result.Value);
+ }
+
+ [Fact]
+ public void AddInMemoryProvider_WithDomainAndNullOptions_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddOptions("domain-name")
+ .Configure((opts) =>
+ {
+ opts.Flags = null;
+ });
+
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider("domain-name");
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetKeyedService("domain-name");
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+ }
+
+ [Fact]
+ public void AddInMemoryProvider_WithDomainAndNullConfigure_AddsProvider()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.AddInMemoryProvider("domain-name", (Action>?)null);
+
+ // Assert
+ using var provider = services.BuildServiceProvider();
+
+ var featureProvider = provider.GetKeyedService("domain-name");
+ Assert.NotNull(featureProvider);
+ Assert.IsType(featureProvider);
+ }
+}
diff --git a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs
index ff717f9f..9638ff8c 100644
--- a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs
+++ b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs
@@ -8,9 +8,9 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using OpenFeature.Constant;
-using OpenFeature.DependencyInjection;
-using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Hooks;
+using OpenFeature.Hosting;
+using OpenFeature.Hosting.Providers.Memory;
using OpenFeature.IntegrationTests.Services;
using OpenFeature.Providers.Memory;
@@ -211,7 +211,6 @@ private static async Task CreateServerAsync(ServiceLifetime serviceL
builder.Services.AddHttpContextAccessor();
builder.Services.AddOpenFeature(cfg =>
{
- cfg.AddHostedFeatureLifecycle();
cfg.AddContext((builder, provider) =>
{
// Retrieve the HttpContext from IHttpContextAccessor, ensuring it's not null.
diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj
index aabe1a59..46f99e21 100644
--- a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj
+++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj
@@ -5,7 +5,14 @@
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
@@ -18,7 +25,6 @@
-