diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6db91397..47330d45 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,6 +9,7 @@
+
@@ -29,7 +30,7 @@
-
+
diff --git a/OpenFeature.sln b/OpenFeature.sln
index e8191acd..ff4cb97e 100644
--- a/OpenFeature.sln
+++ b/OpenFeature.sln
@@ -85,6 +85,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjec
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.IntegrationTests", "test\OpenFeature.IntegrationTests\OpenFeature.IntegrationTests.csproj", "{68463B47-36B4-8DB5-5D02-662C169E85B0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -119,6 +121,10 @@ Global
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {68463B47-36B4-8DB5-5D02-662C169E85B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {68463B47-36B4-8DB5-5D02-662C169E85B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {68463B47-36B4-8DB5-5D02-662C169E85B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {68463B47-36B4-8DB5-5D02-662C169E85B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -137,6 +143,7 @@ Global
{C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
{EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
+ {68463B47-36B4-8DB5-5D02-662C169E85B0} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F}
diff --git a/README.md b/README.md
index acb31e8f..451d68e9 100644
--- a/README.md
+++ b/README.md
@@ -338,41 +338,43 @@ builder.Services.AddOpenFeature(featureBuilder => {
});
});
```
-#### Creating a New Provider
-To integrate a custom provider, such as InMemoryProvider, you’ll need to create a factory that builds and configures the provider. This section demonstrates how to set up InMemoryProvider as a new provider with custom configuration options.
-**Configuring InMemoryProvider as a New Provider**
-
Begin by creating a custom factory class, `InMemoryProviderFactory`, that implements `IFeatureProviderFactory`. This factory will initialize your provider with any necessary configurations.
-```csharp
-public class InMemoryProviderFactory : IFeatureProviderFactory
-{
- internal IDictionary? Flags { get; set; }
-
- public FeatureProvider Create() => new InMemoryProvider(Flags);
-}
-```
-**Adding an Extension Method to OpenFeatureBuilder**
-
To streamline the configuration process, add an extension method, `AddInMemoryProvider`, to `OpenFeatureBuilder`. This allows you to set up the provider with either a domain-scoped or a default configuration.
+### Registering a Custom Provider
+You can register a custom provider, such as `InMemoryProvider`, with OpenFeature using the `AddProvider` method. This approach allows you to dynamically resolve services or configurations during registration.
```csharp
-public static partial class FeatureBuilderExtensions
-{
- public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null)
- => builder.AddProvider(factory => ConfigureFlags(factory, configure));
+services.AddOpenFeature()
+ .AddProvider(provider =>
+ {
+ // Resolve services or configurations as needed
+ var configuration = provider.GetRequiredService();
+ var flags = new Dictionary
+ {
+ { "feature-key", new Flag(configuration.GetValue("FeatureFlags:Key")) }
+ };
+
+ // Register a custom provider, such as InMemoryProvider
+ return new InMemoryProvider(flags);
+ });
+```
- public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null)
- => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure));
+#### Adding a Domain-Scoped Provider
- private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure)
- {
- if (configure == null)
- return;
+You can also register a domain-scoped custom provider, enabling configurations specific to each domain:
- var flag = new Dictionary();
- configure.Invoke(flag);
- factory.Flags = flag;
- }
-}
+```csharp
+services.AddOpenFeature()
+ .AddProvider("my-domain", (provider, domain) =>
+ {
+ // Resolve services or configurations as needed for the domain
+ var flags = new Dictionary
+ {
+ { $"{domain}-feature-key", new Flag(true) }
+ };
+
+ // Register a domain-scoped custom provider such as InMemoryProvider
+ return new InMemoryProvider(flags);
+ });
```
diff --git a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs
deleted file mode 100644
index 8c40cee3..00000000
--- a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace OpenFeature.DependencyInjection;
-
-///
-/// Provides a contract for creating instances of .
-/// This factory interface enables custom configuration and initialization of feature providers
-/// to support domain-specific or application-specific feature flag management.
-///
-#if NET8_0_OR_GREATER
-[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
-#endif
-public interface IFeatureProviderFactory
-{
- ///
- /// Creates an instance of a configured according to
- /// the specific settings implemented by the concrete factory.
- ///
- ///
- /// A new instance of .
- /// The configuration and behavior of this provider instance are determined by
- /// the implementation of this method.
- ///
- FeatureProvider Create();
-}
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
index a494b045..a9c3f258 100644
--- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
@@ -21,9 +21,7 @@ public static partial class OpenFeatureBuilderExtensions
/// the desired configuration
/// The instance.
/// Thrown when the or action is null.
- public static OpenFeatureBuilder AddContext(
- this OpenFeatureBuilder builder,
- Action configure)
+ public static OpenFeatureBuilder AddContext(this OpenFeatureBuilder builder, Action configure)
{
Guard.ThrowIfNull(builder);
Guard.ThrowIfNull(configure);
@@ -38,9 +36,7 @@ public static OpenFeatureBuilder AddContext(
/// the desired configuration
/// The instance.
/// Thrown when the or action is null.
- public static OpenFeatureBuilder AddContext(
- this OpenFeatureBuilder builder,
- Action configure)
+ public static OpenFeatureBuilder AddContext(this OpenFeatureBuilder builder, Action configure)
{
Guard.ThrowIfNull(builder);
Guard.ThrowIfNull(configure);
@@ -57,122 +53,106 @@ public static OpenFeatureBuilder AddContext(
}
///
- /// Adds a new feature provider with specified options and configuration 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 type for configuring the feature provider.
- /// The type of the provider factory implementing .
- /// The instance.
- /// An optional action to configure the provider factory of type .
- /// The instance.
- /// Thrown when the is null.
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null)
+ /// 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
- where TProviderFactory : class, IFeatureProviderFactory
{
Guard.ThrowIfNull(builder);
builder.HasDefaultProvider = true;
-
- builder.Services.Configure(options =>
- {
- options.AddDefaultProviderName();
- });
-
- if (configureFactory != null)
+ builder.Services.PostConfigure(options => options.AddDefaultProviderName());
+ if (configureOptions != null)
{
- builder.Services.AddOptions()
- .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.")
- .Configure(configureFactory);
+ builder.Services.Configure(configureOptions);
}
- else
- {
- builder.Services.AddOptions()
- .Configure(options => { });
- }
-
- builder.Services.TryAddSingleton(static provider =>
- {
- var providerFactory = provider.GetRequiredService>().Value;
- return providerFactory.Create();
- });
+ builder.Services.TryAddTransient(implementationFactory);
builder.AddClient();
-
return builder;
}
///
- /// Adds a new feature provider with the default type and a specified configuration builder.
- ///
- /// The type of the provider factory implementing .
- /// The instance.
- /// An optional action to configure the provider factory of type .
- /// The configured instance.
- /// Thrown when the is null.
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null)
- where TProviderFactory : class, IFeatureProviderFactory
- => AddProvider(builder, configureFactory);
-
- ///
- /// Adds a feature provider with specified options and configuration builder for the specified domain.
+ /// Adds a feature provider for a specific domain using provided options and a configuration builder.
///
- /// The type for configuring the feature provider.
- /// The type of the provider factory implementing .
- /// The instance.
+ /// Type derived from used to configure the feature provider.
+ /// The used to configure feature flags.
/// The unique name of the provider.
- /// An optional action to configure the provider factory of type .
- /// The instance.
- /// Thrown when the or is null or empty.
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null)
+ ///
+ /// 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
- where TProviderFactory : class, IFeatureProviderFactory
{
Guard.ThrowIfNull(builder);
- Guard.ThrowIfNullOrWhiteSpace(domain, nameof(domain));
builder.DomainBoundProviderRegistrationCount++;
- builder.Services.Configure(options =>
- {
- options.AddProviderName(domain);
- });
-
- if (configureFactory != null)
- {
- builder.Services.AddOptions(domain)
- .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.")
- .Configure(configureFactory);
- }
- else
+ builder.Services.PostConfigure(options => options.AddProviderName(domain));
+ if (configureOptions != null)
{
- builder.Services.AddOptions(domain)
- .Configure(options => { });
+ builder.Services.Configure(domain, configureOptions);
}
- builder.Services.TryAddKeyedSingleton(domain, static (provider, key) =>
+ builder.Services.TryAddKeyedTransient(domain, (provider, key) =>
{
- var options = provider.GetRequiredService>();
- var providerFactory = options.Get(key!.ToString());
- return providerFactory.Create();
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+ return implementationFactory(provider, key.ToString()!);
});
builder.AddClient(domain);
-
return builder;
}
///
- /// Adds a feature provider with a specified configuration builder for the specified domain, using default .
+ /// 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 type of the provider factory implementing .
- /// The instance.
- /// The unique domain of the provider.
- /// An optional action to configure the provider factory of type .
- /// The configured instance.
- /// Thrown when the or is null or empty.
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null)
- where TProviderFactory : class, IFeatureProviderFactory
- => AddProvider(builder, domain, configureFactory);
+ /// 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.
@@ -231,19 +211,24 @@ internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, st
}
///
- /// Configures a default client for OpenFeature using the provided factory function.
+ /// 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.
- ///
- /// A factory function that creates an based on the service provider and .
- ///
/// The configured instance.
- internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func clientFactory)
+ internal static OpenFeatureBuilder AddPolicyBasedClient(this OpenFeatureBuilder builder)
{
builder.Services.AddScoped(provider =>
{
var policy = provider.GetRequiredService>().Value;
- return clientFactory(provider, policy);
+ var name = policy.DefaultNameSelector(provider);
+ if (name == null)
+ {
+ return provider.GetRequiredService();
+ }
+ return provider.GetRequiredKeyedService(name);
});
return builder;
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
index 1be312ed..b2f15e44 100644
--- a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
@@ -26,13 +26,13 @@ public class OpenFeatureOptions
/// Registers the default provider name if no specific name is provided.
/// Sets to true.
///
- public void AddDefaultProviderName() => AddProviderName(null);
+ 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.
- public void AddProviderName(string? name)
+ protected internal void AddProviderName(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
index e7a503bb..74d01ad3 100644
--- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
+++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
@@ -53,16 +53,7 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services
});
}
- builder.AddDefaultClient((provider, policy) =>
- {
- var name = policy.DefaultNameSelector.Invoke(provider);
- if (name == null)
- {
- return provider.GetRequiredService();
- }
- return provider.GetRequiredKeyedService(name);
- });
-
+ builder.AddPolicyBasedClient();
return services;
}
}
diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
index 199e01b0..d6346ad7 100644
--- a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
+++ b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
@@ -1,3 +1,5 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
using OpenFeature.Providers.Memory;
namespace OpenFeature.DependencyInjection.Providers.Memory;
@@ -10,46 +12,115 @@ namespace OpenFeature.DependencyInjection.Providers.Memory;
#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)
+ => AddInMemoryProvider(builder, 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 provided, it allows setting up the initial flags.
+ /// If null, an empty provider will be created.
///
/// The instance for chaining.
public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null)
- => builder.AddProvider(factory => ConfigureFlags(factory, configure));
+ => 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 unique domain of the provider
///
/// An optional delegate to configure feature flags in the in-memory provider.
- /// If provided, it allows setting up the initial flags.
+ /// 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, factory => ConfigureFlags(factory, configure));
+ => builder.AddProvider(domain, CreateProvider, options => ConfigureFlags(options, configure));
- ///
- /// Configures the feature flags for an instance.
- ///
- /// The to configure.
- ///
- /// An optional delegate that sets up the initial flags in the provider's flag dictionary.
- ///
- private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure)
+ private static FeatureProvider CreateProvider(IServiceProvider provider, string domain)
{
- if (configure == null)
- return;
+ var options = provider.GetRequiredService>().Get(domain);
+ if (options.Flags == null)
+ {
+ return new InMemoryProvider();
+ }
- var flag = new Dictionary();
- configure.Invoke(flag);
- factory.Flags = flag;
+ 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.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs
deleted file mode 100644
index 2d155dd9..00000000
--- a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using OpenFeature.Providers.Memory;
-
-namespace OpenFeature.DependencyInjection.Providers.Memory;
-
-///
-/// A factory for creating instances of ,
-/// an in-memory implementation of .
-/// This factory allows for the customization of feature flags to facilitate
-/// testing and lightweight feature flag management without external dependencies.
-///
-#if NET8_0_OR_GREATER
-[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
-#endif
-public class InMemoryProviderFactory : IFeatureProviderFactory
-{
- ///
- /// Gets or sets the collection of feature flags used to configure the
- /// instances. This dictionary maps
- /// flag names to instances, enabling pre-configuration
- /// of features for testing or in-memory evaluation.
- ///
- internal IDictionary? Flags { get; set; }
-
- ///
- /// Creates a new instance of with the specified
- /// flags set in . This instance is configured for in-memory
- /// feature flag management, suitable for testing or lightweight feature toggling scenarios.
- ///
- ///
- /// A configured that can be used to manage
- /// feature flags in an in-memory context.
- ///
- public FeatureProvider Create() => new InMemoryProvider(Flags);
-}
diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs
new file mode 100644
index 00000000..ea5433f4
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs
@@ -0,0 +1,19 @@
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.DependencyInjection.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.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs
deleted file mode 100644
index 1ee14bf0..00000000
--- a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace OpenFeature.DependencyInjection.Tests;
-
-#if NET8_0_OR_GREATER
-[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
-#endif
-public class NoOpFeatureProviderFactory : IFeatureProviderFactory
-{
- public FeatureProvider Create() => new NoOpFeatureProvider();
-}
diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs
index 3f6ef227..087336a0 100644
--- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs
+++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
using OpenFeature.Model;
using Xunit;
@@ -22,12 +23,12 @@ public OpenFeatureBuilderExtensionsTests()
public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate)
{
// Act
- var result = useServiceProviderDelegate ?
+ var featureBuilder = useServiceProviderDelegate ?
_systemUnderTest.AddContext(_ => { }) :
_systemUnderTest.AddContext((_, _) => { });
// Assert
- result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance.");
+ featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance.");
_systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured.");
_services.Should().ContainSingle(serviceDescriptor =>
serviceDescriptor.ServiceType == typeof(EvaluationContext) &&
@@ -61,37 +62,185 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
#endif
- [Fact]
- public void AddProvider_ShouldAddProviderToCollection()
+ [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 result = _systemUnderTest.AddProvider();
+ 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
_systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured.");
- result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance.");
+ _systemUnderTest.HasDefaultProvider.Should().Be(expectsDefaultProvider, "The default provider flag should be set correctly.");
+ _systemUnderTest.IsPolicyConfigured.Should().BeFalse("The policy should not be configured.");
+ _systemUnderTest.DomainBoundProviderRegistrationCount.Should().Be(expectsDomainBoundProvider, "The domain-bound provider count should be correct.");
+ featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance.");
_services.Should().ContainSingle(serviceDescriptor =>
serviceDescriptor.ServiceType == typeof(FeatureProvider) &&
- serviceDescriptor.Lifetime == ServiceLifetime.Singleton,
+ serviceDescriptor.Lifetime == ServiceLifetime.Transient,
"A singleton service of type FeatureProvider should be added.");
}
+ class TestOptions : OpenFeatureOptions { }
+
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)]
#endif
- [Fact]
- public void AddProvider_ShouldResolveCorrectProvider()
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ [InlineData(4)]
+ public void AddProvider_ShouldResolveCorrectProvider(int providerRegistrationType)
{
// Arrange
- _systemUnderTest.AddProvider();
+ _ = 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 = serviceProvider.GetService();
+ var provider = providerRegistrationType switch
+ {
+ 1 or 3 => serviceProvider.GetService(),
+ 2 or 4 => serviceProvider.GetKeyedService("test"),
+ _ => throw new InvalidOperationException("Invalid mode.")
+ };
+
+ // Assert
+ provider.Should().NotBeNull("The FeatureProvider should be resolvable.");
+ provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider.");
+ }
+
+ [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
_systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured.");
+ _systemUnderTest.HasDefaultProvider.Should().Be(expectsDefaultProvider, "The default provider flag should be set correctly.");
+ _systemUnderTest.IsPolicyConfigured.Should().BeFalse("The policy should not be configured.");
+ _systemUnderTest.DomainBoundProviderRegistrationCount.Should().Be(expectsDomainBoundProvider, "The domain-bound provider count should be correct.");
+ featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance.");
+ }
+
+ [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
+ featureBuilder.IsPolicyConfigured.Should().BeTrue("The policy should be configured.");
provider.Should().NotBeNull("The FeatureProvider should be resolvable.");
provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider.");
}
diff --git a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs
new file mode 100644
index 00000000..559bf4bb
--- /dev/null
+++ b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs
@@ -0,0 +1,144 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using FluentAssertions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using OpenFeature.DependencyInjection.Providers.Memory;
+using OpenFeature.IntegrationTests.Services;
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.IntegrationTests;
+
+public class FeatureFlagIntegrationTest
+{
+ // TestUserId is "off", other users are "on"
+ private const string FeatureA = "feature-a";
+ private const string TestUserId = "123";
+
+ [Theory]
+ [InlineData(TestUserId, false, ServiceLifetime.Singleton)]
+ [InlineData(TestUserId, false, ServiceLifetime.Scoped)]
+ [InlineData(TestUserId, false, ServiceLifetime.Transient)]
+ [InlineData("SomeOtherId", true, ServiceLifetime.Singleton)]
+ [InlineData("SomeOtherId", true, ServiceLifetime.Scoped)]
+ [InlineData("SomeOtherId", true, ServiceLifetime.Transient)]
+ public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string userId, bool expectedResult, ServiceLifetime serviceLifetime)
+ {
+ // Arrange
+ using var server = await CreateServerAsync(services =>
+ {
+ switch (serviceLifetime)
+ {
+ case ServiceLifetime.Singleton:
+ services.AddSingleton();
+ break;
+ case ServiceLifetime.Scoped:
+ services.AddScoped();
+ break;
+ case ServiceLifetime.Transient:
+ services.AddTransient();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null);
+ }
+ }).ConfigureAwait(true);
+
+ var client = server.CreateClient();
+ var requestUri = $"/features/{userId}/flags/{FeatureA}";
+
+ // Act
+ var response = await client.GetAsync(requestUri).ConfigureAwait(true);
+ var responseContent = await response.Content.ReadFromJsonAsync>().ConfigureAwait(true); ;
+
+ // Assert
+ response.IsSuccessStatusCode.Should().BeTrue("Expected HTTP status code 200 OK.");
+ responseContent.Should().NotBeNull("Expected response content to be non-null.");
+ responseContent!.FeatureName.Should().Be(FeatureA, "Expected feature name to be 'feature-a'.");
+ responseContent.FeatureValue.Should().Be(expectedResult, "Expected feature value to match the expected result.");
+ }
+
+ private static async Task CreateServerAsync(Action? configureServices = null)
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.WebHost.UseTestServer();
+
+ configureServices?.Invoke(builder.Services);
+ builder.Services.TryAddSingleton();
+
+ builder.Services.AddHttpContextAccessor();
+ builder.Services.AddOpenFeature(cfg =>
+ {
+ cfg.AddHostedFeatureLifecycle();
+ cfg.AddContext((builder, provider) =>
+ {
+ // Retrieve the HttpContext from IHttpContextAccessor, ensuring it's not null.
+ var context = provider.GetRequiredService().HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+
+ var userId = UserInfoHelper.GetUserId(context);
+ builder.Set("user", userId);
+ });
+ cfg.AddInMemoryProvider(provider =>
+ {
+ var flagService = provider.GetRequiredService();
+ return flagService.GetFlags();
+ });
+ });
+
+ var app = builder.Build();
+
+ app.UseRouting();
+ app.Map($"/features/{{userId}}/flags/{{featureName}}", async context =>
+ {
+ var client = context.RequestServices.GetRequiredService();
+ var featureName = UserInfoHelper.GetFeatureName(context);
+ var res = await client.GetBooleanValueAsync(featureName, false).ConfigureAwait(true);
+ var result = await client.GetBooleanValueAsync(featureName, false).ConfigureAwait(true);
+
+ var response = new FeatureFlagResponse(featureName, result);
+
+ // Serialize the response object to JSON
+ var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+
+ // Write the JSON response
+ context.Response.ContentType = "application/json";
+ await context.Response.WriteAsync(jsonResponse).ConfigureAwait(true);
+ });
+
+ await app.StartAsync().ConfigureAwait(true);
+
+ return app.GetTestServer();
+ }
+
+ public class FlagConfigurationService : IFeatureFlagConfigurationService
+ {
+ private readonly IDictionary _flags;
+ public FlagConfigurationService()
+ {
+ _flags = new Dictionary
+ {
+ {
+ "feature-a", new Flag(
+ variants: new Dictionary()
+ {
+ { "on", true },
+ { "off", false }
+ },
+ defaultVariant: "on", context => {
+ var id = context.GetValue("user").AsString;
+ if(id == null)
+ {
+ return "on"; // default variant
+ }
+
+ return id == TestUserId ? "off" : "on";
+ })
+ }
+ };
+ }
+ public Dictionary GetFlags() => _flags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+ }
+}
diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj
new file mode 100644
index 00000000..8287b2ec
--- /dev/null
+++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenFeature.IntegrationTests/Services/FeatureFlagResponse.cs b/test/OpenFeature.IntegrationTests/Services/FeatureFlagResponse.cs
new file mode 100644
index 00000000..50285cc0
--- /dev/null
+++ b/test/OpenFeature.IntegrationTests/Services/FeatureFlagResponse.cs
@@ -0,0 +1,3 @@
+namespace OpenFeature.IntegrationTests.Services;
+
+public record FeatureFlagResponse(string FeatureName, T FeatureValue) where T : notnull;
diff --git a/test/OpenFeature.IntegrationTests/Services/IFeatureFlagConfigurationService.cs b/test/OpenFeature.IntegrationTests/Services/IFeatureFlagConfigurationService.cs
new file mode 100644
index 00000000..1b51a60a
--- /dev/null
+++ b/test/OpenFeature.IntegrationTests/Services/IFeatureFlagConfigurationService.cs
@@ -0,0 +1,8 @@
+using OpenFeature.Providers.Memory;
+
+namespace OpenFeature.IntegrationTests.Services;
+
+internal interface IFeatureFlagConfigurationService
+{
+ Dictionary GetFlags();
+}
diff --git a/test/OpenFeature.IntegrationTests/Services/UserInfo.cs b/test/OpenFeature.IntegrationTests/Services/UserInfo.cs
new file mode 100644
index 00000000..c2c5d8c1
--- /dev/null
+++ b/test/OpenFeature.IntegrationTests/Services/UserInfo.cs
@@ -0,0 +1,3 @@
+namespace OpenFeature.IntegrationTests.Services;
+
+public record UserInfo(string UserId, string FeatureName);
diff --git a/test/OpenFeature.IntegrationTests/Services/UserInfoHelper.cs b/test/OpenFeature.IntegrationTests/Services/UserInfoHelper.cs
new file mode 100644
index 00000000..0e057a6b
--- /dev/null
+++ b/test/OpenFeature.IntegrationTests/Services/UserInfoHelper.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Http;
+
+namespace OpenFeature.IntegrationTests.Services;
+
+public static class UserInfoHelper
+{
+ ///
+ /// Extracts the user ID from the HTTP request context.
+ ///
+ /// The HTTP context containing the request.
+ /// The user ID as a string.
+ /// Thrown if the user ID is not found in the route values.
+ public static string GetUserId(HttpContext context)
+ {
+ if (context.Request.RouteValues.TryGetValue("userId", out var userId) && userId is string userIdString)
+ {
+ return userIdString;
+ }
+ throw new ArgumentNullException(nameof(userId), "User ID not found in route values.");
+ }
+
+ ///
+ /// Extracts the feature name from the HTTP request context.
+ ///
+ /// The HTTP context containing the request.
+ /// The feature name as a string.
+ /// Thrown if the feature name is not found in the route values.
+ public static string GetFeatureName(HttpContext context)
+ {
+ if (context.Request.RouteValues.TryGetValue("featureName", out var featureName) && featureName is string featureNameString)
+ {
+ return featureNameString;
+ }
+ throw new ArgumentNullException(nameof(featureName), "Feature name not found in route values.");
+ }
+}