diff --git a/OpenFeature.slnx b/OpenFeature.slnx index 936079f40..a9e90dcab 100644 --- a/OpenFeature.slnx +++ b/OpenFeature.slnx @@ -48,6 +48,7 @@ + diff --git a/src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs b/src/OpenFeature.DependencyInjection.Abstractions/Diagnostics/FeatureCodes.cs similarity index 93% rename from src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs rename to src/OpenFeature.DependencyInjection.Abstractions/Diagnostics/FeatureCodes.cs index f7ecf81cb..e24b83c66 100644 --- a/src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs +++ b/src/OpenFeature.DependencyInjection.Abstractions/Diagnostics/FeatureCodes.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.Hosting.Diagnostics; +namespace OpenFeature.DependencyInjection.Abstractions.Diagnostics; /// /// Contains identifiers for experimental features and diagnostics in the OpenFeature framework. @@ -22,7 +22,7 @@ namespace OpenFeature.Hosting.Diagnostics; /// - "001" - Unique identifier for a specific feature. /// /// -internal static class FeatureCodes +public static class FeatureCodes { /// /// Identifier for the experimental Dependency Injection features within the OpenFeature framework. diff --git a/src/OpenFeature.DependencyInjection.Abstractions/OpenFeature.DependencyInjection.Abstractions.csproj b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeature.DependencyInjection.Abstractions.csproj new file mode 100644 index 000000000..be60d49d9 --- /dev/null +++ b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeature.DependencyInjection.Abstractions.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0;net8.0;net9.0;net462 + OpenFeature.DependencyInjection.Abstractions + + + + + + + + + + + + + + + + + diff --git a/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderBuilder.cs b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderBuilder.cs new file mode 100644 index 000000000..5ff17f47d --- /dev/null +++ b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderBuilder.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature.DependencyInjection.Abstractions; + +/// +/// Describes a backed by an . +/// +public abstract class OpenFeatureProviderBuilder(IServiceCollection services) +{ + /// The services being configured. + public IServiceCollection Services { get; } = services; + + /// + /// 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; } + + /// + /// Indicates whether the policy has been configured. + /// + public bool IsPolicyConfigured { 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) + { + return; + } + + 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."); + } + } + + /// + /// Adds an IFeatureClient to the container. If is supplied, + /// registers a domain-bound client; otherwise registers a global client. If an evaluation context is + /// configured, it is applied at resolve-time. + /// + /// The current . + internal protected abstract OpenFeatureProviderBuilder TryAddClient(string? name = null); +} diff --git a/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderBuilderExtensions.cs b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderBuilderExtensions.cs new file mode 100644 index 000000000..02e4dc86b --- /dev/null +++ b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderBuilderExtensions.cs @@ -0,0 +1,145 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.DependencyInjection.Abstractions; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(DependencyInjection.Abstractions.Diagnostics.FeatureCodes.NewDi)] +#endif +public static partial class OpenFeatureProviderBuilderExtensions +{ + /// + /// 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 OpenFeatureProviderBuilder AddProvider(this OpenFeatureProviderBuilder 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 OpenFeatureProviderBuilder AddProvider(this OpenFeatureProviderBuilder builder, Func implementationFactory, Action? configureOptions) + where TOptions : OpenFeatureProviderOptions + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.HasDefaultProvider = true; + builder.Services.PostConfigure(options => options.AddDefaultProviderName()); + if (configureOptions != null) + { + builder.Services.Configure(configureOptions); + } + + builder.Services.TryAddTransient(implementationFactory); + builder.TryAddClient(); + 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 OpenFeatureProviderBuilder AddProvider(this OpenFeatureProviderBuilder builder, string domain, Func implementationFactory, Action? configureOptions) + where TOptions : OpenFeatureProviderOptions + { + if (builder == null) throw new ArgumentNullException(nameof(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.TryAddClient(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 OpenFeatureProviderBuilder AddProvider(this OpenFeatureProviderBuilder builder, string domain, Func implementationFactory) + => AddProvider(builder, domain, implementationFactory, configureOptions: null); + + /// + /// 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 OpenFeatureProviderBuilder AddPolicyName(this OpenFeatureProviderBuilder builder, Action configureOptions) + where TOptions : PolicyNameOptions + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + if (configureOptions == null) throw new ArgumentNullException(nameof(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 OpenFeatureProviderBuilder AddPolicyName(this OpenFeatureProviderBuilder builder, Action configureOptions) + => AddPolicyName(builder, configureOptions); +} diff --git a/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderOptions.cs b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderOptions.cs new file mode 100644 index 000000000..218204dfe --- /dev/null +++ b/src/OpenFeature.DependencyInjection.Abstractions/OpenFeatureProviderOptions.cs @@ -0,0 +1,61 @@ +using System.Collections.ObjectModel; + +namespace OpenFeature.DependencyInjection.Abstractions; + +/// +/// Provider-focused options for configuring OpenFeature integrations. +/// Contains only contracts and metadata that integrations may need. +/// +public class OpenFeatureProviderOptions +{ + private readonly HashSet _providerNames = []; + + /// + /// Determines if a default provider has been registered. + /// + public bool HasDefaultProvider { get; private set; } + + /// + /// The of the configured feature provider, if any. + /// Typically set by higher-level configuration. + /// + public Type FeatureProviderType { get; protected internal set; } = null!; + + /// + /// Gets a read-only list of registered provider names. + /// + public IReadOnlyCollection ProviderNames + { + get + { + lock (_providerNames) + { + return new ReadOnlyCollection([.. _providerNames]); + } + } + } + + /// + /// Registers the default provider name if no specific name is provided. + /// Sets to true. + /// + 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. + internal void AddProviderName(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + HasDefaultProvider = true; + return; + } + + lock (_providerNames) + { + _providerNames.Add(name!); + } + } +} diff --git a/src/OpenFeature.Hosting/PolicyNameOptions.cs b/src/OpenFeature.DependencyInjection.Abstractions/PolicyNameOptions.cs similarity index 84% rename from src/OpenFeature.Hosting/PolicyNameOptions.cs rename to src/OpenFeature.DependencyInjection.Abstractions/PolicyNameOptions.cs index 3dfa76f89..8ea167e43 100644 --- a/src/OpenFeature.Hosting/PolicyNameOptions.cs +++ b/src/OpenFeature.DependencyInjection.Abstractions/PolicyNameOptions.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.Hosting; +namespace OpenFeature.DependencyInjection.Abstractions; /// /// Options to configure the default feature client name. diff --git a/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs index 4d915946b..332bc5ad4 100644 --- a/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection.Abstractions; namespace OpenFeature.Hosting.Internal; @@ -22,7 +23,14 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke { this.LogStartingInitializationOfFeatureProvider(); - var options = _serviceProvider.GetRequiredService>().Value; + await InitializeProvidersAsync(cancellationToken).ConfigureAwait(false); + InitializeHooks(); + InitializeHandlers(); + } + + private async Task InitializeProvidersAsync(CancellationToken cancellationToken) + { + var options = _serviceProvider.GetRequiredService>().Value; if (options.HasDefaultProvider) { var featureProvider = _serviceProvider.GetRequiredService(); @@ -34,7 +42,11 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke var featureProvider = _serviceProvider.GetRequiredKeyedService(name); await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false); } + } + private void InitializeHooks() + { + var options = _serviceProvider.GetRequiredService>().Value; var hooks = new List(); foreach (var hookName in options.HookNames) { @@ -43,7 +55,10 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke } _featureApi.AddHooks(hooks); + } + private void InitializeHandlers() + { var handlers = _serviceProvider.GetServices(); foreach (var handler in handlers) { diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj index 84e5efa61..81f6d87f1 100644 --- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -11,6 +11,7 @@ + diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilder.cs b/src/OpenFeature.Hosting/OpenFeatureBuilder.cs index 177a9fac3..aeaee6073 100644 --- a/src/OpenFeature.Hosting/OpenFeatureBuilder.cs +++ b/src/OpenFeature.Hosting/OpenFeatureBuilder.cs @@ -1,4 +1,7 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.DependencyInjection.Abstractions; +using OpenFeature.Model; namespace OpenFeature.Hosting; @@ -6,11 +9,8 @@ namespace OpenFeature.Hosting; /// Describes a backed by an . /// /// The services being configured. -public class OpenFeatureBuilder(IServiceCollection services) +public class OpenFeatureBuilder(IServiceCollection services) : OpenFeatureProviderBuilder(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 @@ -19,42 +19,76 @@ public class OpenFeatureBuilder(IServiceCollection services) 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. + /// Internal convenience API to add a client by name (or the default client when is null/empty). + /// Delegates to the overridable . /// - public bool HasDefaultProvider { get; internal set; } + /// Optional key for a named client registration. + /// The current . + internal OpenFeatureProviderBuilder AddClient(string? name = null) + => TryAddClient(name); /// - /// Gets the count of domain-bound providers that have been registered. - /// This count does not include the default provider. + /// Adds an to the container, optionally keyed by . + /// If an evaluation context is configured, the client is created with that context. /// - public int DomainBoundProviderRegistrationCount { get; internal set; } + /// Optional key for a named client registration. + /// The current . + protected override OpenFeatureProviderBuilder TryAddClient(string? name = null) + => string.IsNullOrWhiteSpace(name) ? AddClient() : AddDomainBoundClient(name!); /// - /// 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. + /// Adds a global (domain-agnostic) . + /// The evaluation context (if configured) is resolved per scope at resolve-time. /// - /// - /// 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() + /// The current . + private OpenFeatureProviderBuilder AddClient() { - if (!IsPolicyConfigured) + if (IsContextConfigured) { - if (DomainBoundProviderRegistrationCount > 1) + Services.TryAddScoped(static provider => { - throw new InvalidOperationException("Multiple providers have been registered, but no policy has been configured."); - } + 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(); + }); + } - if (HasDefaultProvider && DomainBoundProviderRegistrationCount == 1) + return this; + } + + /// + private OpenFeatureProviderBuilder AddDomainBoundClient(string name) + { + if (IsContextConfigured) + { + Services.TryAddKeyedScoped(name, static (provider, key) => { - throw new InvalidOperationException("A default provider and an additional provider have been registered without a policy configuration."); - } + var api = provider.GetRequiredService(); + var client = api.GetClient(key!.ToString()); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); } + else + { + Services.TryAddKeyedScoped(name, static (provider, key) => + { + var api = provider.GetRequiredService(); + return api.GetClient(key!.ToString()); + }); + } + + return this; } } diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs index 52c66c42e..6714ecf61 100644 --- a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OpenFeature.Constant; +using OpenFeature.DependencyInjection.Abstractions; using OpenFeature.Hosting; using OpenFeature.Hosting.Internal; using OpenFeature.Model; @@ -51,164 +52,6 @@ public static OpenFeatureBuilder AddContext(this OpenFeatureBuilder builder, Act 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 @@ -233,35 +76,6 @@ internal static OpenFeatureBuilder AddPolicyBasedClient(this OpenFeatureBuilder 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. /// diff --git a/src/OpenFeature.Hosting/OpenFeatureOptions.cs b/src/OpenFeature.Hosting/OpenFeatureOptions.cs index 9d3dd818e..be4788378 100644 --- a/src/OpenFeature.Hosting/OpenFeatureOptions.cs +++ b/src/OpenFeature.Hosting/OpenFeatureOptions.cs @@ -5,48 +5,6 @@ namespace OpenFeature.Hosting; /// 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; diff --git a/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs index 236dc62b0..ba2f1b0d0 100644 --- a/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection.Abstractions; using OpenFeature.Hosting; using OpenFeature.Hosting.Internal; @@ -49,7 +50,7 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services { options.DefaultNameSelector = provider => { - var options = provider.GetRequiredService>().Value; + var options = provider.GetRequiredService>().Value; return options.ProviderNames.First(); }; }); diff --git a/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs index d63009d62..68d55bb83 100644 --- a/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs +++ b/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs @@ -1,27 +1,28 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection.Abstractions; using OpenFeature.Providers.Memory; namespace OpenFeature.Hosting.Providers.Memory; /// -/// Extension methods for configuring feature providers with . +/// Extension methods for configuring feature providers with . /// #if NET8_0_OR_GREATER -[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +[System.Diagnostics.CodeAnalysis.Experimental(DependencyInjection.Abstractions.Diagnostics.FeatureCodes.NewDi)] #endif public static partial class FeatureBuilderExtensions { /// - /// Adds an in-memory feature provider to the with a factory for flags. + /// Adds an in-memory feature provider to the with a factory for flags. /// - /// The instance to configure. + /// 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) + /// The instance for chaining. + public static OpenFeatureProviderBuilder AddInMemoryProvider(this OpenFeatureProviderBuilder builder, Func?> flagsFactory) => builder.AddProvider(provider => { var flags = flagsFactory(provider); @@ -34,29 +35,29 @@ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder bui }); /// - /// Adds an in-memory feature provider to the with a domain and factory for flags. + /// Adds an in-memory feature provider to the with a domain and factory for flags. /// - /// The instance to configure. + /// 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) + /// The instance for chaining. + public static OpenFeatureProviderBuilder AddInMemoryProvider(this OpenFeatureProviderBuilder 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. + /// 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 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) + /// The instance for chaining. + public static OpenFeatureProviderBuilder AddInMemoryProvider(this OpenFeatureProviderBuilder builder, string domain, Func?> flagsFactory) => builder.AddProvider(domain, (provider, key) => { var flags = flagsFactory(provider, key); @@ -69,28 +70,28 @@ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder bui }); /// - /// Adds an in-memory feature provider to the with optional flag configuration. + /// Adds an in-memory feature provider to the with optional flag configuration. /// - /// The instance to configure. + /// 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) + /// The instance for chaining. + public static OpenFeatureProviderBuilder AddInMemoryProvider(this OpenFeatureProviderBuilder 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. + /// Adds an in-memory feature provider with a specific domain to the with optional flag configuration. /// - /// The instance to configure. + /// 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) + /// The instance for chaining. + public static OpenFeatureProviderBuilder AddInMemoryProvider(this OpenFeatureProviderBuilder builder, string domain, Action>? configure = null) => builder.AddProvider(domain, CreateProvider, options => ConfigureFlags(options, configure)); private static FeatureProvider CreateProvider(IServiceProvider provider, string domain) diff --git a/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs b/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs index 3e7431eef..15310905c 100644 --- a/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs +++ b/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs @@ -1,3 +1,4 @@ +using OpenFeature.DependencyInjection.Abstractions; using OpenFeature.Providers.Memory; namespace OpenFeature.Hosting.Providers.Memory; @@ -5,7 +6,7 @@ namespace OpenFeature.Hosting.Providers.Memory; /// /// Options for configuring the in-memory feature flag provider. /// -public class InMemoryProviderOptions : OpenFeatureOptions +public class InMemoryProviderOptions : OpenFeatureProviderOptions { /// /// Gets or sets the feature flags to be used by the in-memory provider. diff --git a/test/OpenFeature.Hosting.Tests/Internal/FeatureLifecycleManagerTests.cs b/test/OpenFeature.Hosting.Tests/Internal/FeatureLifecycleManagerTests.cs index 2d379fc4e..9437ae1b9 100644 --- a/test/OpenFeature.Hosting.Tests/Internal/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.Hosting.Tests/Internal/FeatureLifecycleManagerTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; using OpenFeature.Constant; +using OpenFeature.DependencyInjection.Abstractions; using OpenFeature.Hosting.Internal; namespace OpenFeature.Hosting.Tests.Internal; @@ -15,7 +16,7 @@ public async Task EnsureInitializedAsync_SetsProvider() // Arrange var services = new ServiceCollection(); var provider = new NoOpFeatureProvider(); - services.AddOptions().Configure(options => + services.AddOptions().Configure(options => { options.AddProviderName(null); }); @@ -40,7 +41,7 @@ public async Task EnsureInitializedAsync_SetsMultipleProvider() var services = new ServiceCollection(); var provider1 = new NoOpFeatureProvider(); var provider2 = new NoOpFeatureProvider(); - services.AddOptions().Configure(options => + services.AddOptions().Configure(options => { options.AddProviderName("provider1"); options.AddProviderName("provider2"); @@ -67,9 +68,12 @@ public async Task EnsureInitializedAsync_AddsHooks() var services = new ServiceCollection(); var provider = new NoOpFeatureProvider(); var hook = new NoOpHook(); - services.AddOptions().Configure(options => + services.AddOptions().Configure(options => { options.AddProviderName(null); + }); + services.AddOptions().Configure(options => + { options.AddHookName("TestHook"); }); services.AddSingleton(provider); @@ -94,7 +98,7 @@ public async Task EnsureInitializedAsync_AddHandlers() // Arrange var services = new ServiceCollection(); var provider = new NoOpFeatureProvider(); - services.AddOptions().Configure(options => + services.AddOptions().Configure(options => { options.AddProviderName(null); }); @@ -142,7 +146,7 @@ public async Task EnsureInitializedAsync_LogStartingInitialization() // Arrange var services = new ServiceCollection(); var provider = new NoOpFeatureProvider(); - services.AddOptions().Configure(options => + services.AddOptions().Configure(options => { options.AddProviderName(null); }); @@ -169,7 +173,7 @@ public async Task ShutdownAsync_LogShuttingDown() // Arrange var services = new ServiceCollection(); var provider = new NoOpFeatureProvider(); - services.AddOptions().Configure(options => + services.AddOptions().Configure(options => { options.AddProviderName(null); }); diff --git a/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderExtensionsTests.cs index 1a284c918..0485eb313 100644 --- a/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.Hosting.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection.Abstractions; using OpenFeature.Hosting.Internal; using OpenFeature.Model; @@ -58,7 +59,7 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe } #if NET8_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] + [System.Diagnostics.CodeAnalysis.Experimental(DependencyInjection.Abstractions.Diagnostics.FeatureCodes.NewDi)] #endif [Theory] [InlineData(1, true, 0)] @@ -72,8 +73,8 @@ public void AddProvider_ShouldAddProviderToCollection(int providerRegistrationTy { 1 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()), 2 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider()), - 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { }), - 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }), + 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { o.SomeFlag = true; }), + 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { o.SomeFlag = true; }), _ => throw new InvalidOperationException("Invalid mode.") }; @@ -88,10 +89,13 @@ public void AddProvider_ShouldAddProviderToCollection(int providerRegistrationTy serviceDescriptor.Lifetime == ServiceLifetime.Transient); } - class TestOptions : OpenFeatureOptions { } + internal sealed class TestProviderOptions : OpenFeatureProviderOptions + { + public bool SomeFlag { get; set; } + } #if NET8_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] + [System.Diagnostics.CodeAnalysis.Experimental(DependencyInjection.Abstractions.Diagnostics.FeatureCodes.NewDi)] #endif [Theory] [InlineData(1)] @@ -105,8 +109,8 @@ public void AddProvider_ShouldResolveCorrectProvider(int providerRegistrationTyp { 1 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()), 2 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider()), - 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { }), - 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }), + 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { }), + 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }), _ => throw new InvalidOperationException("Invalid mode.") }; @@ -149,22 +153,22 @@ public void AddProvider_VerifiesDefaultAndDomainBoundProvidersBasedOnConfigurati .AddProvider("test1", (_, _) => new NoOpFeatureProvider()) .AddProvider("test2", (_, _) => new NoOpFeatureProvider()), 4 => _systemUnderTest - .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) .AddProvider("test", (_, _) => new NoOpFeatureProvider()), 5 => _systemUnderTest - .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) .AddProvider("test", (_, _) => new NoOpFeatureProvider()), 6 => _systemUnderTest - .AddProvider("test1", (_, _) => new NoOpFeatureProvider(), o => { }) + .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 => { }), + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider(), o => { }), _ => throw new InvalidOperationException("Invalid mode.") }; @@ -203,15 +207,15 @@ public void AddProvider_ConfiguresPolicyNameAcrossMultipleProviderSetups(int pro .AddProvider("test2", (_, _) => new NoOpFeatureProvider()) .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), 4 => _systemUnderTest - .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) .AddProvider("test", (_, _) => new NoOpFeatureProvider()) .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), 5 => _systemUnderTest - .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) .AddProvider("test", (_, _) => new NoOpFeatureProvider()) .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), 6 => _systemUnderTest - .AddProvider("test1", (_, _) => new NoOpFeatureProvider(), o => { }) + .AddProvider("test1", (_, _) => new NoOpFeatureProvider(), o => { }) .AddProvider("test2", (_, _) => new NoOpFeatureProvider()) .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), 7 => _systemUnderTest @@ -220,9 +224,9 @@ public void AddProvider_ConfiguresPolicyNameAcrossMultipleProviderSetups(int pro .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 => { }) + .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.") }; diff --git a/test/OpenFeature.Hosting.Tests/OpenFeatureOptionsTests.cs b/test/OpenFeature.Hosting.Tests/OpenFeatureProviderOptionsTests.cs similarity index 81% rename from test/OpenFeature.Hosting.Tests/OpenFeatureOptionsTests.cs rename to test/OpenFeature.Hosting.Tests/OpenFeatureProviderOptionsTests.cs index d39d4059f..7a254ec1a 100644 --- a/test/OpenFeature.Hosting.Tests/OpenFeatureOptionsTests.cs +++ b/test/OpenFeature.Hosting.Tests/OpenFeatureProviderOptionsTests.cs @@ -1,12 +1,14 @@ +using OpenFeature.DependencyInjection.Abstractions; + namespace OpenFeature.Hosting.Tests; -public class OpenFeatureOptionsTests +public class OpenFeatureProviderOptionsTests { [Fact] public void AddProviderName_DoesNotSetHasDefaultProvider() { // Arrange - var options = new OpenFeatureOptions(); + var options = new OpenFeatureProviderOptions(); // Act options.AddProviderName("TestProvider"); @@ -19,7 +21,7 @@ public void AddProviderName_DoesNotSetHasDefaultProvider() public void AddProviderName_WithNullName_SetsHasDefaultProvider() { // Arrange - var options = new OpenFeatureOptions(); + var options = new OpenFeatureProviderOptions(); // Act options.AddProviderName(null); @@ -34,7 +36,7 @@ public void AddProviderName_WithNullName_SetsHasDefaultProvider() public void AddProviderName_WithEmptyName_SetsHasDefaultProvider(string name) { // Arrange - var options = new OpenFeatureOptions(); + var options = new OpenFeatureProviderOptions(); // Act options.AddProviderName(name); @@ -47,7 +49,7 @@ public void AddProviderName_WithEmptyName_SetsHasDefaultProvider(string name) public void AddProviderName_WithSameName_OnlyRegistersNameOnce() { // Arrange - var options = new OpenFeatureOptions(); + var options = new OpenFeatureProviderOptions(); // Act options.AddProviderName("test-provider"); diff --git a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs index 9638ff8c1..82a286cca 100644 --- a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs +++ b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs @@ -224,8 +224,8 @@ private static async Task CreateServerAsync(ServiceLifetime serviceL { if (serviceLifetime == ServiceLifetime.Scoped) { - using var scoped = provider.CreateScope(); - var flagService = scoped.ServiceProvider.GetRequiredService(); + using var scope = provider.CreateScope(); + var flagService = scope.ServiceProvider.GetRequiredService(); return flagService.GetFlags(); } else