diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs index 3dc0203b1..67eb52439 100644 --- a/samples/AspNetCore/Program.cs +++ b/samples/AspNetCore/Program.cs @@ -7,6 +7,7 @@ using OpenFeature.Model; using OpenFeature.Providers.Memory; using OpenFeature.Providers.MultiProvider; +using OpenFeature.Providers.MultiProvider.DependencyInjection; using OpenFeature.Providers.MultiProvider.Models; using OpenFeature.Providers.MultiProvider.Strategies; using OpenTelemetry.Metrics; @@ -59,7 +60,28 @@ { "disable", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 0).Build()) } }, "disable") } - }); + }) + .AddMultiProvider("multi-provider", multiProviderBuilder => + { + // Create provider flags + var provider1Flags = new Dictionary + { + { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider1" }, { "disabled", "disabled-provider1" } }, "enabled") }, + { "max-items", new Flag(new Dictionary { { "low", 10 }, { "high", 100 } }, "high") }, + }; + + var provider2Flags = new Dictionary + { + { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") }, + }; + + // Use the factory pattern to create providers - they will be properly initialized + multiProviderBuilder + .AddProvider("p1", sp => new InMemoryProvider(provider1Flags)) + .AddProvider("p2", sp => new InMemoryProvider(provider2Flags)) + .UseStrategy(); + }) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => "InMemory"); }); var app = builder.Build(); @@ -139,6 +161,27 @@ } }); +app.MapGet("/multi-provider-di", async ([FromServices] Api openFeatureApi) => +{ + try + { + var featureClient = openFeatureApi.GetClient("multi-provider"); + + // Test flag evaluation from different providers + var maxItemsFlag = await featureClient.GetIntegerDetailsAsync("max-items", 0); + var providerNameFlag = await featureClient.GetStringDetailsAsync("providername", "default"); + + // Test a flag that doesn't exist in any provider + var unknownFlag = await featureClient.GetBooleanDetailsAsync("unknown-flag", false); + + return Results.Ok(); + } + catch (Exception ex) + { + return Results.Problem($"Error: {ex.Message}\n\nStack: {ex.StackTrace}"); + } +}); + app.Run(); diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs new file mode 100644 index 000000000..5f2f9ad8e --- /dev/null +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs @@ -0,0 +1,94 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenFeature.Hosting; + +namespace OpenFeature.Providers.MultiProvider.DependencyInjection; + +/// +/// Extension methods for configuring the multi-provider with . +/// +public static class FeatureBuilderExtensions +{ + /// + /// Adds a multi-provider to the with a configuration builder. + /// + /// The instance to configure. + /// + /// A delegate to configure the multi-provider using the . + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddMultiProvider( + this OpenFeatureBuilder builder, + Action configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + return builder.AddProvider( + serviceProvider => CreateMultiProviderFromConfigure(serviceProvider, configure)); + } + + /// + /// Adds a multi-provider with a specific domain to the with a configuration builder. + /// + /// The instance to configure. + /// The unique domain of the provider. + /// + /// A delegate to configure the multi-provider using the . + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddMultiProvider( + this OpenFeatureBuilder builder, + string domain, + Action configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (string.IsNullOrWhiteSpace(domain)) + { + throw new ArgumentException("Domain cannot be null or empty.", nameof(domain)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + return builder.AddProvider( + domain, + (serviceProvider, _) => CreateMultiProviderFromConfigure(serviceProvider, configure)); + } + + private static MultiProvider CreateMultiProviderFromConfigure(IServiceProvider serviceProvider, Action configure) + { + // Build the multi-provider configuration using the builder + var multiProviderBuilder = new MultiProviderBuilder(); + + // Apply the configuration action + configure(multiProviderBuilder); + + // Build provider entries and strategy from the builder using the service provider + var providerEntries = multiProviderBuilder.BuildProviderEntries(serviceProvider); + var evaluationStrategy = multiProviderBuilder.BuildEvaluationStrategy(serviceProvider); + + if (providerEntries.Count == 0) + { + throw new InvalidOperationException("At least one provider must be configured for the multi-provider."); + } + + // Get logger from DI + var logger = serviceProvider.GetService>(); + + return new MultiProvider(providerEntries, evaluationStrategy, logger); + } +} diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs new file mode 100644 index 000000000..b586300e0 --- /dev/null +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs @@ -0,0 +1,139 @@ +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.Providers.MultiProvider.Models; +using OpenFeature.Providers.MultiProvider.Strategies; + +namespace OpenFeature.Providers.MultiProvider.DependencyInjection; + +/// +/// Builder for configuring a multi-provider with dependency injection. +/// +public class MultiProviderBuilder +{ + private readonly List> _providerFactories = []; + private Func? _strategyFactory; + + /// + /// Adds a provider to the multi-provider configuration using a factory method. + /// + /// The name for the provider. + /// A factory method to create the provider instance. + /// The instance for chaining. + public MultiProviderBuilder AddProvider(string name, Func factory) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Provider name cannot be null or empty.", nameof(name)); + } + + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + this._providerFactories.Add(sp => new ProviderEntry(factory(sp), name)); + return this; + } + + /// + /// Adds a provider to the multi-provider configuration using a type. + /// + /// The type of the provider to add. + /// The name for the provider. + /// An optional factory method to create the provider instance. If not provided, the provider will be resolved from the service provider. + /// The instance for chaining. + public MultiProviderBuilder AddProvider(string name, Func? factory = null) + where TProvider : FeatureProvider + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Provider name cannot be null or empty.", nameof(name)); + } + + this._providerFactories.Add(sp => + { + var provider = factory != null + ? factory(sp) + : sp.GetRequiredService(); + return new ProviderEntry(provider, name); + }); + + return this; + } + + /// + /// Adds a provider instance to the multi-provider configuration. + /// + /// The name for the provider. + /// The provider instance to add. + /// The instance for chaining. + public MultiProviderBuilder AddProvider(string name, FeatureProvider provider) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Provider name cannot be null or empty.", nameof(name)); + } + + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + this._providerFactories.Add(_ => new ProviderEntry(provider, name)); + return this; + } + + /// + /// Sets the evaluation strategy for the multi-provider. + /// + /// The type of the evaluation strategy. + /// The instance for chaining. + public MultiProviderBuilder UseStrategy() + where TStrategy : BaseEvaluationStrategy, new() + { + this._strategyFactory = _ => new TStrategy(); + return this; + } + + /// + /// Sets the evaluation strategy for the multi-provider using a factory method. + /// + /// A factory method to create the strategy instance. + /// The instance for chaining. + public MultiProviderBuilder UseStrategy(Func factory) + { + this._strategyFactory = factory ?? throw new ArgumentNullException(nameof(factory)); + return this; + } + + /// + /// Sets the evaluation strategy for the multi-provider. + /// + /// The strategy instance to use. + /// The instance for chaining. + public MultiProviderBuilder UseStrategy(BaseEvaluationStrategy strategy) + { + if (strategy == null) + { + throw new ArgumentNullException(nameof(strategy)); + } + + this._strategyFactory = _ => strategy; + return this; + } + + /// + /// Builds the provider entries using the service provider. + /// + internal List BuildProviderEntries(IServiceProvider serviceProvider) + { + return this._providerFactories.Select(factory => factory(serviceProvider)).ToList(); + } + + /// + /// Builds the evaluation strategy using the service provider. + /// + internal BaseEvaluationStrategy? BuildEvaluationStrategy(IServiceProvider serviceProvider) + { + return this._strategyFactory?.Invoke(serviceProvider); + } +} diff --git a/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj b/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj index d999c5613..99d30b4a7 100644 --- a/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj +++ b/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj @@ -1,17 +1,18 @@  - - OpenFeature.Providers.MultiProvider - README.md - + + OpenFeature.Providers.MultiProvider + README.md + - - - - - + + + + + - - - + + + + \ No newline at end of file diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj index d416bd75b..823d96f0e 100644 --- a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj +++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj @@ -18,7 +18,7 @@ - + diff --git a/test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderBuilderTests.cs b/test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderBuilderTests.cs new file mode 100644 index 000000000..40fbb6b12 --- /dev/null +++ b/test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderBuilderTests.cs @@ -0,0 +1,327 @@ +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using OpenFeature.Model; +using OpenFeature.Providers.MultiProvider.DependencyInjection; +using OpenFeature.Providers.MultiProvider.Strategies; +using OpenFeature.Providers.MultiProvider.Tests.Utils; + +namespace OpenFeature.Providers.MultiProvider.Tests.DependencyInjection; + +public class MultiProviderBuilderTests +{ + private readonly FeatureProvider _mockProvider = Substitute.For(); + + public MultiProviderBuilderTests() + { + _mockProvider.GetMetadata().Returns(new Metadata("mock-provider")); + } + + [Fact] + public void AddProvider_WithNullName_ThrowsArgumentException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.AddProvider(null!, _mockProvider)); + } + + [Fact] + public void AddProvider_WithEmptyName_ThrowsArgumentException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.AddProvider("", _mockProvider)); + } + + [Fact] + public void AddProvider_WithNullProvider_ThrowsArgumentNullException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.AddProvider("test", (FeatureProvider)null!)); + } + + [Fact] + public void AddProvider_WithFactory_WithNullName_ThrowsArgumentException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.AddProvider(null!, sp => _mockProvider)); + } + + [Fact] + public void AddProvider_WithFactory_WithNullFactory_ThrowsArgumentNullException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.AddProvider("test", (Func)null!)); + } + + [Fact] + public void AddProvider_Generic_WithNullName_ThrowsArgumentException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.AddProvider(null!)); + } + + [Fact] + public void AddProvider_Generic_WithEmptyName_ThrowsArgumentException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.AddProvider("")); + } + + [Fact] + public void AddProvider_AddsProviderToBuilder() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + builder.AddProvider("test-provider", _mockProvider); + var entries = builder.BuildProviderEntries(serviceProvider); + + // Assert + Assert.Single(entries); + Assert.Equal("test-provider", entries[0].Name); + Assert.Same(_mockProvider, entries[0].Provider); + } + + [Fact] + public void AddProvider_WithFactory_AddsProviderToBuilder() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + builder.AddProvider("test-provider", sp => _mockProvider); + var entries = builder.BuildProviderEntries(serviceProvider); + + // Assert + Assert.Single(entries); + Assert.Equal("test-provider", entries[0].Name); + Assert.Same(_mockProvider, entries[0].Provider); + } + + [Fact] + public void AddProvider_Generic_WithFactory_AddsProviderToBuilder() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + builder.AddProvider("test-provider", sp => new TestProvider("test-provider")); + var entries = builder.BuildProviderEntries(serviceProvider); + + // Assert + Assert.Single(entries); + Assert.Equal("test-provider", entries[0].Name); + Assert.IsType(entries[0].Provider); + } + + [Fact] + public void AddProvider_Generic_WithoutFactory_ResolvesFromServiceProvider() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + services.AddTransient(_ => new TestProvider("test-provider")); + var serviceProvider = services.BuildServiceProvider(); + + // Act + builder.AddProvider("test-provider"); + var entries = builder.BuildProviderEntries(serviceProvider); + + // Assert + Assert.Single(entries); + Assert.Equal("test-provider", entries[0].Name); + Assert.IsType(entries[0].Provider); + } + + [Fact] + public void AddProvider_MultipleProviders_AddsAllProviders() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + var provider1 = Substitute.For(); + var provider2 = Substitute.For(); + var provider3 = Substitute.For(); + + provider1.GetMetadata().Returns(new Metadata("provider1")); + provider2.GetMetadata().Returns(new Metadata("provider2")); + provider3.GetMetadata().Returns(new Metadata("provider3")); + + // Act + builder + .AddProvider("provider1", provider1) + .AddProvider("provider2", sp => provider2) + .AddProvider("provider3", sp => new TestProvider("provider3")); + + var entries = builder.BuildProviderEntries(serviceProvider); + + // Assert + Assert.Equal(3, entries.Count); + Assert.Equal("provider1", entries[0].Name); + Assert.Equal("provider2", entries[1].Name); + Assert.Equal("provider3", entries[2].Name); + } + + [Fact] + public void UseStrategy_Generic_SetsStrategy() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + builder.UseStrategy(); + var strategy = builder.BuildEvaluationStrategy(serviceProvider); + + // Assert + Assert.NotNull(strategy); + Assert.IsType(strategy); + } + + [Fact] + public void UseStrategy_WithInstance_SetsStrategy() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var strategyInstance = new FirstMatchStrategy(); + + // Act + builder.UseStrategy(strategyInstance); + var strategy = builder.BuildEvaluationStrategy(serviceProvider); + + // Assert + Assert.NotNull(strategy); + Assert.Same(strategyInstance, strategy); + } + + [Fact] + public void UseStrategy_WithNullInstance_ThrowsArgumentNullException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.UseStrategy((FirstMatchStrategy)null!)); + } + + [Fact] + public void UseStrategy_WithFactory_SetsStrategy() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + builder.UseStrategy(sp => new FirstMatchStrategy()); + var strategy = builder.BuildEvaluationStrategy(serviceProvider); + + // Assert + Assert.NotNull(strategy); + Assert.IsType(strategy); + } + + [Fact] + public void UseStrategy_WithNullFactory_ThrowsArgumentNullException() + { + // Arrange + var builder = new MultiProviderBuilder(); + + // Act & Assert + Assert.Throws(() => + builder.UseStrategy((Func)null!)); + } + + [Fact] + public void BuildEvaluationStrategy_WithNoStrategy_ReturnsNull() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + var strategy = builder.BuildEvaluationStrategy(serviceProvider); + + // Assert + Assert.Null(strategy); + } + + [Fact] + public void BuildProviderEntries_WithNoProviders_ReturnsEmptyList() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + var entries = builder.BuildProviderEntries(serviceProvider); + + // Assert + Assert.NotNull(entries); + Assert.Empty(entries); + } + + [Fact] + public void Builder_ChainsMethodsCorrectly() + { + // Arrange + var builder = new MultiProviderBuilder(); + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + var result = builder + .AddProvider("provider1", _mockProvider) + .AddProvider("provider2", sp => _mockProvider) + .UseStrategy(); + + var entries = builder.BuildProviderEntries(serviceProvider); + var strategy = builder.BuildEvaluationStrategy(serviceProvider); + + // Assert + Assert.Same(builder, result); + Assert.Equal(2, entries.Count); + Assert.NotNull(strategy); + } +} diff --git a/test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderDependencyInjectionTests.cs b/test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderDependencyInjectionTests.cs new file mode 100644 index 000000000..5d2b6dbca --- /dev/null +++ b/test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderDependencyInjectionTests.cs @@ -0,0 +1,292 @@ +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using OpenFeature.Hosting; +using OpenFeature.Model; +using OpenFeature.Providers.MultiProvider.DependencyInjection; +using OpenFeature.Providers.MultiProvider.Strategies; +using OpenFeature.Providers.MultiProvider.Tests.Utils; + +namespace OpenFeature.Providers.MultiProvider.Tests.DependencyInjection; + +public class MultiProviderDependencyInjectionTests +{ + private readonly FeatureProvider _mockProvider = Substitute.For(); + + public MultiProviderDependencyInjectionTests() + { + _mockProvider.GetMetadata().Returns(new Metadata("test-provider")); + _mockProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new ResolutionDetails("test", true))); + } + + [Fact] + public void AddMultiProvider_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null!; + + // Act & Assert + Assert.Throws(() => + builder.AddMultiProvider(b => b.AddProvider("test", _mockProvider))); + } + + [Fact] + public void AddMultiProvider_WithNullConfigure_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => + builder.AddMultiProvider(null!)); + } + + [Fact] + public void AddMultiProvider_WithDomain_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null!; + + // Act & Assert + Assert.Throws(() => + builder.AddMultiProvider("test-domain", b => b.AddProvider("test", _mockProvider))); + } + + [Fact] + public void AddMultiProvider_WithDomain_WithNullDomain_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => + builder.AddMultiProvider(null!, b => b.AddProvider("test", _mockProvider))); + } + + [Fact] + public void AddMultiProvider_WithDomain_WithEmptyDomain_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => + builder.AddMultiProvider("", b => b.AddProvider("test", _mockProvider))); + } + + [Fact] + public void AddMultiProvider_WithDomain_WithNullConfigure_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => + builder.AddMultiProvider("test-domain", null!)); + } + + [Fact] + public void AddMultiProvider_WithNoProviders_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection(); + services.AddOpenFeature(builder => + { + builder.AddMultiProvider(b => { }); // Empty configuration + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act & Assert + Assert.Throws(() => + serviceProvider.GetRequiredService()); + } + + [Fact] + public void AddMultiProvider_RegistersProviderCorrectly() + { + // Arrange + var services = new ServiceCollection(); + services.AddOpenFeature(builder => + { + builder.AddMultiProvider(b => + { + b.AddProvider("provider1", _mockProvider); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(provider); + Assert.IsType(provider); + } + + [Fact] + public void AddMultiProvider_WithDomain_RegistersProviderCorrectly() + { + // Arrange + var services = new ServiceCollection(); + services.AddOpenFeature(builder => + { + builder.AddMultiProvider("test-domain", b => + { + b.AddProvider("provider1", _mockProvider); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetRequiredKeyedService("test-domain"); + + // Assert + Assert.NotNull(provider); + Assert.IsType(provider); + } + + [Fact] + public void AddMultiProvider_WithMultipleProviders_CreatesMultiProvider() + { + // Arrange + var services = new ServiceCollection(); + var provider1 = Substitute.For(); + var provider2 = Substitute.For(); + var provider3 = Substitute.For(); + + provider1.GetMetadata().Returns(new Metadata("provider1")); + provider2.GetMetadata().Returns(new Metadata("provider2")); + provider3.GetMetadata().Returns(new Metadata("provider3")); + + services.AddOpenFeature(builder => + { + builder.AddMultiProvider(b => + { + b.AddProvider("provider1", provider1) + .AddProvider("provider2", provider2) + .AddProvider("provider3", provider3); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(provider); + Assert.IsType(provider); + } + + [Fact] + public void AddMultiProvider_WithStrategy_CreatesMultiProviderWithStrategy() + { + // Arrange + var services = new ServiceCollection(); + services.AddOpenFeature(builder => + { + builder.AddMultiProvider(b => + { + b.AddProvider("provider1", _mockProvider) + .UseStrategy(); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(provider); + Assert.IsType(provider); + } + + [Fact] + public void AddMultiProvider_WithFactoryProvider_CreatesProviderFromFactory() + { + // Arrange + var services = new ServiceCollection(); + var factoryCalled = false; + + services.AddOpenFeature(builder => + { + builder.AddMultiProvider(b => + { + b.AddProvider("provider1", sp => + { + factoryCalled = true; + var mockProvider = Substitute.For(); + mockProvider.GetMetadata().Returns(new Metadata("factory-provider")); + return mockProvider; + }); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(provider); + Assert.True(factoryCalled, "Factory method should have been called"); + } + + [Fact] + public void AddMultiProvider_WithTypedProvider_ResolvesFromServiceProvider() + { + // Arrange + var services = new ServiceCollection(); + services.AddTransient(_ => new TestProvider("test-provider")); + + services.AddOpenFeature(builder => + { + builder.AddMultiProvider(b => + { + b.AddProvider("provider1"); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(provider); + Assert.IsType(provider); + } + + [Fact] + public void AddMultiProvider_WithLogger_CreatesMultiProvider() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddOpenFeature(builder => + { + builder.AddMultiProvider(b => + { + b.AddProvider("provider1", _mockProvider); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(provider); + Assert.IsType(provider); + } +}