From a42ea110822ba1b76856cdda25bb6cf37a66170c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:26:55 +0000 Subject: [PATCH 01/10] feat: Implement multi-provider configuration with dependency injection support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FeatureBuilderExtensions.cs | 96 ++++++++++++ .../MultiProviderBuilder.cs | 139 ++++++++++++++++++ .../MultiProviderOptions.cs | 22 +++ ...OpenFeature.Providers.MultiProvider.csproj | 25 ++-- 4 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs create mode 100644 src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs create mode 100644 src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderOptions.cs diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs new file mode 100644 index 00000000..595403da --- /dev/null +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs @@ -0,0 +1,96 @@ +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), + null); + } + + /// + /// 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), + null); + } + + 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 00000000..b586300e --- /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/DependencyInjection/MultiProviderOptions.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderOptions.cs new file mode 100644 index 00000000..5b5a501b --- /dev/null +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderOptions.cs @@ -0,0 +1,22 @@ +using OpenFeature.Hosting; +using OpenFeature.Providers.MultiProvider.Models; +using OpenFeature.Providers.MultiProvider.Strategies; + +namespace OpenFeature.Providers.MultiProvider.DependencyInjection; + +/// +/// Options for configuring the multi-provider. +/// +public class MultiProviderOptions : OpenFeatureOptions +{ + /// + /// Gets or sets the list of provider entries for the multi-provider. + /// + public List ProviderEntries { get; set; } = []; + + /// + /// Gets or sets the evaluation strategy to use for the multi-provider. + /// If not set, the FirstMatchStrategy will be used by default. + /// + public BaseEvaluationStrategy? EvaluationStrategy { get; set; } +} diff --git a/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj b/src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj index d999c561..99d30b4a 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 From 148484ebf15b2b3c6c6fa2353594e12098735d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 1 Nov 2025 17:08:50 +0000 Subject: [PATCH 02/10] feat: Add multi-provider support with dependency injection and flag evaluation endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- samples/AspNetCore/Program.cs | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs index 3dc0203b..c8c35e0c 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,9 +60,33 @@ { "disable", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 0).Build()) } }, "disable") } - }); + }) + .AddMultiProvider("multi-provider", multiProviderBuilder => CreateMultiProviderBuilder(multiProviderBuilder)) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => "InMemory"); }); +static void CreateMultiProviderBuilder(MultiProviderBuilder multiProviderBuilder) +{ + // Create first in-memory provider with some 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 provider1 = new InMemoryProvider(provider1Flags); + + // Create second in-memory provider with different flags + var provider2Flags = new Dictionary + { + { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") }, + }; + var provider2 = new InMemoryProvider(provider2Flags); + + multiProviderBuilder.AddProvider("p1", provider1) + .AddProvider("p2", provider2) + .UseStrategy(); +} + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -139,6 +164,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) + { + return Results.InternalServerError(); + } +}); + app.Run(); From 411a491d62e747561cd147c8e8f36e99c3e7138a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 1 Nov 2025 17:32:37 +0000 Subject: [PATCH 03/10] refactor: Simplify AddMultiProvider method by removing redundant parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- samples/AspNetCore/Program.cs | 47 +++++++++---------- .../FeatureBuilderExtensions.cs | 10 ++-- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs index c8c35e0c..67eb5243 100644 --- a/samples/AspNetCore/Program.cs +++ b/samples/AspNetCore/Program.cs @@ -61,32 +61,29 @@ }, "disable") } }) - .AddMultiProvider("multi-provider", multiProviderBuilder => CreateMultiProviderBuilder(multiProviderBuilder)) + .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"); }); -static void CreateMultiProviderBuilder(MultiProviderBuilder multiProviderBuilder) -{ - // Create first in-memory provider with some 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 provider1 = new InMemoryProvider(provider1Flags); - - // Create second in-memory provider with different flags - var provider2Flags = new Dictionary - { - { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") }, - }; - var provider2 = new InMemoryProvider(provider2Flags); - - multiProviderBuilder.AddProvider("p1", provider1) - .AddProvider("p2", provider2) - .UseStrategy(); -} - var app = builder.Build(); // Configure the HTTP request pipeline. @@ -179,9 +176,9 @@ static void CreateMultiProviderBuilder(MultiProviderBuilder multiProviderBuilder return Results.Ok(); } - catch (Exception) + catch (Exception ex) { - return Results.InternalServerError(); + return Results.Problem($"Error: {ex.Message}\n\nStack: {ex.StackTrace}"); } }); diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs index 595403da..5f2f9ad8 100644 --- a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs @@ -31,9 +31,8 @@ public static OpenFeatureBuilder AddMultiProvider( throw new ArgumentNullException(nameof(configure)); } - return builder.AddProvider( - serviceProvider => CreateMultiProviderFromConfigure(serviceProvider, configure), - null); + return builder.AddProvider( + serviceProvider => CreateMultiProviderFromConfigure(serviceProvider, configure)); } /// @@ -65,10 +64,9 @@ public static OpenFeatureBuilder AddMultiProvider( throw new ArgumentNullException(nameof(configure)); } - return builder.AddProvider( + return builder.AddProvider( domain, - (serviceProvider, _) => CreateMultiProviderFromConfigure(serviceProvider, configure), - null); + (serviceProvider, _) => CreateMultiProviderFromConfigure(serviceProvider, configure)); } private static MultiProvider CreateMultiProviderFromConfigure(IServiceProvider serviceProvider, Action configure) From 49102d09ee6cb25f88595ed6a5501698e51bbc8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 1 Nov 2025 19:07:14 +0000 Subject: [PATCH 04/10] test: Add unit tests for MultiProviderBuilder and MultiProviderDependencyInjection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../MultiProviderBuilderTests.cs | 327 ++++++++++++++++++ .../MultiProviderDependencyInjectionTests.cs | 292 ++++++++++++++++ 2 files changed, 619 insertions(+) create mode 100644 test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderBuilderTests.cs create mode 100644 test/OpenFeature.Providers.MultiProvider.Tests/DependencyInjection/MultiProviderDependencyInjectionTests.cs 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 00000000..40fbb6b1 --- /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 00000000..5d2b6dbc --- /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); + } +} From 73679e1a054d9fa18c3d6da917b5868f93376ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 1 Nov 2025 19:10:36 +0000 Subject: [PATCH 05/10] fix: Update project reference to use OpenFeature.Hosting instead of OpenFeature.DependencyInjection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeature.AotCompatibility.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj index d416bd75..823d96f0 100644 --- a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj +++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj @@ -18,7 +18,7 @@ - + From 4e3bcbb404dcfe3dd0f49edd866e7843019ba55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 1 Nov 2025 19:11:45 +0000 Subject: [PATCH 06/10] refactor: Remove MultiProviderOptions class as part of code cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../MultiProviderOptions.cs | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderOptions.cs diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderOptions.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderOptions.cs deleted file mode 100644 index 5b5a501b..00000000 --- a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using OpenFeature.Hosting; -using OpenFeature.Providers.MultiProvider.Models; -using OpenFeature.Providers.MultiProvider.Strategies; - -namespace OpenFeature.Providers.MultiProvider.DependencyInjection; - -/// -/// Options for configuring the multi-provider. -/// -public class MultiProviderOptions : OpenFeatureOptions -{ - /// - /// Gets or sets the list of provider entries for the multi-provider. - /// - public List ProviderEntries { get; set; } = []; - - /// - /// Gets or sets the evaluation strategy to use for the multi-provider. - /// If not set, the FirstMatchStrategy will be used by default. - /// - public BaseEvaluationStrategy? EvaluationStrategy { get; set; } -} From 3d068d0629d3de26a8406b61e091b6240cf375e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:31:11 +0000 Subject: [PATCH 07/10] docs: add dependency injection documentation to MultiProvider README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Document AddMultiProvider extension methods - Explain MultiProviderBuilder usage patterns - Show examples for adding providers via factory, instance, and DI - Document evaluation strategy configuration - Add domain-scoped provider configuration examples - Position DI setup as recommended approach Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../README.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/OpenFeature.Providers.MultiProvider/README.md b/src/OpenFeature.Providers.MultiProvider/README.md index 8b12807c..00108c0a 100644 --- a/src/OpenFeature.Providers.MultiProvider/README.md +++ b/src/OpenFeature.Providers.MultiProvider/README.md @@ -18,8 +18,91 @@ dotnet add package OpenFeature.Providers.MultiProvider ## Usage +### Dependency Injection Setup (Recommended) + +The MultiProvider integrates seamlessly with the OpenFeature dependency injection system, allowing you to configure multiple providers using the `AddMultiProvider` extension method: + +```csharp +using OpenFeature.Providers.MultiProvider.DependencyInjection; + +builder.Services.AddOpenFeature(featureBuilder => +{ + featureBuilder + .AddMultiProvider("multi-provider", multiProviderBuilder => + { + // Add providers using factory methods for proper DI integration + multiProviderBuilder + .AddProvider("primary", sp => new YourPrimaryProvider()) + .AddProvider("fallback", sp => new YourFallbackProvider()) + .UseStrategy(); + }); +}); + +// Retrieve and use the client +var featureClient = openFeatureApi.GetClient("multi-provider"); +var result = await featureClient.GetBooleanValueAsync("my-flag", false); +``` + +#### Adding Providers with DI + +The `MultiProviderBuilder` provides several methods to add providers: + +**Using Factory Methods:** +```csharp +multiProviderBuilder + .AddProvider("provider-name", sp => new InMemoryProvider(flags)) + .AddProvider("another-provider", sp => sp.GetRequiredService()); +``` + +**Using Provider Instances:** +```csharp +var provider = new InMemoryProvider(flags); +multiProviderBuilder.AddProvider("provider-name", provider); +``` + +**Using Generic Type Resolution:** +```csharp +// Provider will be resolved from DI container +multiProviderBuilder.AddProvider("provider-name"); + +// Or with custom factory +multiProviderBuilder.AddProvider("provider-name", sp => new YourProvider(config)); +``` + +#### Configuring Evaluation Strategy + +Specify an evaluation strategy using any of these methods: + +```csharp +// Using generic type +multiProviderBuilder.UseStrategy(); + +// Using factory method with DI +multiProviderBuilder.UseStrategy(sp => new FirstMatchStrategy()); + +// Using strategy instance +multiProviderBuilder.UseStrategy(new ComparisonStrategy()); +``` + +#### Using with Named Domains + +Configure the MultiProvider for a specific domain: + +```csharp +featureBuilder + .AddMultiProvider("production-domain", multiProviderBuilder => + { + multiProviderBuilder + .AddProvider("remote", sp => new RemoteProvider()) + .AddProvider("cache", sp => new CacheProvider()) + .UseStrategy(); + }); +``` + ### Basic Setup +For scenarios where dependency injection is not available, you can use the traditional setup: + ```csharp using OpenFeature; using OpenFeature.Providers.MultiProvider; From 1b2ef7cffdbb5ab70751d228f7c2c096bde84387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:03:43 +0000 Subject: [PATCH 08/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kyle <38759683+kylejuliandev@users.noreply.github.com> Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- samples/AspNetCore/Program.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs index 67eb5243..6651e4fb 100644 --- a/samples/AspNetCore/Program.cs +++ b/samples/AspNetCore/Program.cs @@ -161,12 +161,10 @@ } }); -app.MapGet("/multi-provider-di", async ([FromServices] Api openFeatureApi) => +app.MapGet("/multi-provider-di", async ([FromKeyedServices("multi-provider")] IFeatureClient featureClient) => { 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"); From 3c1eb53a345f7c0881ae00d3ee2f70b44d3f7a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:17:59 +0000 Subject: [PATCH 09/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kyle <38759683+kylejuliandev@users.noreply.github.com> Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../DependencyInjection/MultiProviderBuilder.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs index b586300e..08e48677 100644 --- a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs @@ -30,8 +30,7 @@ public MultiProviderBuilder AddProvider(string name, Func new ProviderEntry(factory(sp), name)); - return this; + return AddProvider(name, sp => factory(sp)); } /// @@ -78,8 +77,7 @@ public MultiProviderBuilder AddProvider(string name, FeatureProvider provider) throw new ArgumentNullException(nameof(provider)); } - this._providerFactories.Add(_ => new ProviderEntry(provider, name)); - return this; + return AddProvider(name, _ => provider); } /// @@ -90,8 +88,7 @@ public MultiProviderBuilder AddProvider(string name, FeatureProvider provider) public MultiProviderBuilder UseStrategy() where TStrategy : BaseEvaluationStrategy, new() { - this._strategyFactory = _ => new TStrategy(); - return this; + return UseStrategy(static _ => new TStrategy()); } /// @@ -117,8 +114,7 @@ public MultiProviderBuilder UseStrategy(BaseEvaluationStrategy strategy) throw new ArgumentNullException(nameof(strategy)); } - this._strategyFactory = _ => strategy; - return this; + return UseStrategy(_ => strategy); } /// From 2656177bb33ac85e1e420fc543c0f8afb1c4282b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:26:34 +0000 Subject: [PATCH 10/10] fix: Improve null argument exception messages in MultiProvider configuration Signed-off-by: GitHub --- .../DependencyInjection/FeatureBuilderExtensions.cs | 2 +- .../DependencyInjection/MultiProviderBuilder.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs index 5f2f9ad8..12d61c25 100644 --- a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs @@ -61,7 +61,7 @@ public static OpenFeatureBuilder AddMultiProvider( if (configure == null) { - throw new ArgumentNullException(nameof(configure)); + throw new ArgumentNullException(nameof(configure), "Configure action cannot be null. Please provide a valid configuration for the multi-provider."); } return builder.AddProvider( diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs index 08e48677..3353e612 100644 --- a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs +++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs @@ -27,7 +27,7 @@ public MultiProviderBuilder AddProvider(string name, Func(name, sp => factory(sp)); @@ -74,7 +74,7 @@ public MultiProviderBuilder AddProvider(string name, FeatureProvider provider) if (provider == null) { - throw new ArgumentNullException(nameof(provider)); + throw new ArgumentNullException(nameof(provider), "Provider configuration cannot be null."); } return AddProvider(name, _ => provider); @@ -98,7 +98,7 @@ public MultiProviderBuilder UseStrategy() /// The instance for chaining. public MultiProviderBuilder UseStrategy(Func factory) { - this._strategyFactory = factory ?? throw new ArgumentNullException(nameof(factory)); + this._strategyFactory = factory ?? throw new ArgumentNullException(nameof(factory), "Strategy for multi-provider cannot be null."); return this; }