Skip to content

Commit ee862f0

Browse files
askptkylejuliandev
andauthored
feat: Add DI for multi provider (#621)
* feat: Implement multi-provider configuration with dependency injection support Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * feat: Add multi-provider support with dependency injection and flag evaluation endpoints Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * refactor: Simplify AddMultiProvider method by removing redundant parameters Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * test: Add unit tests for MultiProviderBuilder and MultiProviderDependencyInjection Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * fix: Update project reference to use OpenFeature.Hosting instead of OpenFeature.DependencyInjection Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * refactor: Remove MultiProviderOptions class as part of code cleanup Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * docs: add dependency injection documentation to MultiProvider README - 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> * Apply suggestions from code review Co-authored-by: Kyle <38759683+kylejuliandev@users.noreply.github.com> Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Kyle <38759683+kylejuliandev@users.noreply.github.com> Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * fix: Improve null argument exception messages in MultiProvider configuration Signed-off-by: GitHub <noreply@github.com> --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: GitHub <noreply@github.com> Co-authored-by: Kyle <38759683+kylejuliandev@users.noreply.github.com>
1 parent 7805184 commit ee862f0

File tree

8 files changed

+987
-14
lines changed

8 files changed

+987
-14
lines changed

samples/AspNetCore/Program.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using OpenFeature.Model;
88
using OpenFeature.Providers.Memory;
99
using OpenFeature.Providers.MultiProvider;
10+
using OpenFeature.Providers.MultiProvider.DependencyInjection;
1011
using OpenFeature.Providers.MultiProvider.Models;
1112
using OpenFeature.Providers.MultiProvider.Strategies;
1213
using OpenTelemetry.Metrics;
@@ -59,7 +60,28 @@
5960
{ "disable", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 0).Build()) }
6061
}, "disable")
6162
}
62-
});
63+
})
64+
.AddMultiProvider("multi-provider", multiProviderBuilder =>
65+
{
66+
// Create provider flags
67+
var provider1Flags = new Dictionary<string, Flag>
68+
{
69+
{ "providername", new Flag<string>(new Dictionary<string, string> { { "enabled", "enabled-provider1" }, { "disabled", "disabled-provider1" } }, "enabled") },
70+
{ "max-items", new Flag<int>(new Dictionary<string, int> { { "low", 10 }, { "high", 100 } }, "high") },
71+
};
72+
73+
var provider2Flags = new Dictionary<string, Flag>
74+
{
75+
{ "providername", new Flag<string>(new Dictionary<string, string> { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") },
76+
};
77+
78+
// Use the factory pattern to create providers - they will be properly initialized
79+
multiProviderBuilder
80+
.AddProvider("p1", sp => new InMemoryProvider(provider1Flags))
81+
.AddProvider("p2", sp => new InMemoryProvider(provider2Flags))
82+
.UseStrategy<FirstMatchStrategy>();
83+
})
84+
.AddPolicyName(policy => policy.DefaultNameSelector = provider => "InMemory");
6385
});
6486

6587
var app = builder.Build();
@@ -139,6 +161,25 @@
139161
}
140162
});
141163

164+
app.MapGet("/multi-provider-di", async ([FromKeyedServices("multi-provider")] IFeatureClient featureClient) =>
165+
{
166+
try
167+
{
168+
// Test flag evaluation from different providers
169+
var maxItemsFlag = await featureClient.GetIntegerDetailsAsync("max-items", 0);
170+
var providerNameFlag = await featureClient.GetStringDetailsAsync("providername", "default");
171+
172+
// Test a flag that doesn't exist in any provider
173+
var unknownFlag = await featureClient.GetBooleanDetailsAsync("unknown-flag", false);
174+
175+
return Results.Ok();
176+
}
177+
catch (Exception ex)
178+
{
179+
return Results.Problem($"Error: {ex.Message}\n\nStack: {ex.StackTrace}");
180+
}
181+
});
182+
142183
app.Run();
143184

144185

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Logging;
3+
using OpenFeature.Hosting;
4+
5+
namespace OpenFeature.Providers.MultiProvider.DependencyInjection;
6+
7+
/// <summary>
8+
/// Extension methods for configuring the multi-provider with <see cref="OpenFeatureBuilder"/>.
9+
/// </summary>
10+
public static class FeatureBuilderExtensions
11+
{
12+
/// <summary>
13+
/// Adds a multi-provider to the <see cref="OpenFeatureBuilder"/> with a configuration builder.
14+
/// </summary>
15+
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance to configure.</param>
16+
/// <param name="configure">
17+
/// A delegate to configure the multi-provider using the <see cref="MultiProviderBuilder"/>.
18+
/// </param>
19+
/// <returns>The <see cref="OpenFeatureBuilder"/> instance for chaining.</returns>
20+
public static OpenFeatureBuilder AddMultiProvider(
21+
this OpenFeatureBuilder builder,
22+
Action<MultiProviderBuilder> configure)
23+
{
24+
if (builder == null)
25+
{
26+
throw new ArgumentNullException(nameof(builder));
27+
}
28+
29+
if (configure == null)
30+
{
31+
throw new ArgumentNullException(nameof(configure));
32+
}
33+
34+
return builder.AddProvider(
35+
serviceProvider => CreateMultiProviderFromConfigure(serviceProvider, configure));
36+
}
37+
38+
/// <summary>
39+
/// Adds a multi-provider with a specific domain to the <see cref="OpenFeatureBuilder"/> with a configuration builder.
40+
/// </summary>
41+
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance to configure.</param>
42+
/// <param name="domain">The unique domain of the provider.</param>
43+
/// <param name="configure">
44+
/// A delegate to configure the multi-provider using the <see cref="MultiProviderBuilder"/>.
45+
/// </param>
46+
/// <returns>The <see cref="OpenFeatureBuilder"/> instance for chaining.</returns>
47+
public static OpenFeatureBuilder AddMultiProvider(
48+
this OpenFeatureBuilder builder,
49+
string domain,
50+
Action<MultiProviderBuilder> configure)
51+
{
52+
if (builder == null)
53+
{
54+
throw new ArgumentNullException(nameof(builder));
55+
}
56+
57+
if (string.IsNullOrWhiteSpace(domain))
58+
{
59+
throw new ArgumentException("Domain cannot be null or empty.", nameof(domain));
60+
}
61+
62+
if (configure == null)
63+
{
64+
throw new ArgumentNullException(nameof(configure), "Configure action cannot be null. Please provide a valid configuration for the multi-provider.");
65+
}
66+
67+
return builder.AddProvider(
68+
domain,
69+
(serviceProvider, _) => CreateMultiProviderFromConfigure(serviceProvider, configure));
70+
}
71+
72+
private static MultiProvider CreateMultiProviderFromConfigure(IServiceProvider serviceProvider, Action<MultiProviderBuilder> configure)
73+
{
74+
// Build the multi-provider configuration using the builder
75+
var multiProviderBuilder = new MultiProviderBuilder();
76+
77+
// Apply the configuration action
78+
configure(multiProviderBuilder);
79+
80+
// Build provider entries and strategy from the builder using the service provider
81+
var providerEntries = multiProviderBuilder.BuildProviderEntries(serviceProvider);
82+
var evaluationStrategy = multiProviderBuilder.BuildEvaluationStrategy(serviceProvider);
83+
84+
if (providerEntries.Count == 0)
85+
{
86+
throw new InvalidOperationException("At least one provider must be configured for the multi-provider.");
87+
}
88+
89+
// Get logger from DI
90+
var logger = serviceProvider.GetService<ILogger<MultiProvider>>();
91+
92+
return new MultiProvider(providerEntries, evaluationStrategy, logger);
93+
}
94+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using OpenFeature.Providers.MultiProvider.Models;
3+
using OpenFeature.Providers.MultiProvider.Strategies;
4+
5+
namespace OpenFeature.Providers.MultiProvider.DependencyInjection;
6+
7+
/// <summary>
8+
/// Builder for configuring a multi-provider with dependency injection.
9+
/// </summary>
10+
public class MultiProviderBuilder
11+
{
12+
private readonly List<Func<IServiceProvider, ProviderEntry>> _providerFactories = [];
13+
private Func<IServiceProvider, BaseEvaluationStrategy>? _strategyFactory;
14+
15+
/// <summary>
16+
/// Adds a provider to the multi-provider configuration using a factory method.
17+
/// </summary>
18+
/// <param name="name">The name for the provider.</param>
19+
/// <param name="factory">A factory method to create the provider instance.</param>
20+
/// <returns>The <see cref="MultiProviderBuilder"/> instance for chaining.</returns>
21+
public MultiProviderBuilder AddProvider(string name, Func<IServiceProvider, FeatureProvider> factory)
22+
{
23+
if (string.IsNullOrWhiteSpace(name))
24+
{
25+
throw new ArgumentException("Provider name cannot be null or empty.", nameof(name));
26+
}
27+
28+
if (factory == null)
29+
{
30+
throw new ArgumentNullException(nameof(factory), "Provider configuration cannot be null.");
31+
}
32+
33+
return AddProvider<FeatureProvider>(name, sp => factory(sp));
34+
}
35+
36+
/// <summary>
37+
/// Adds a provider to the multi-provider configuration using a type.
38+
/// </summary>
39+
/// <typeparam name="TProvider">The type of the provider to add.</typeparam>
40+
/// <param name="name">The name for the provider.</param>
41+
/// <param name="factory">An optional factory method to create the provider instance. If not provided, the provider will be resolved from the service provider.</param>
42+
/// <returns>The <see cref="MultiProviderBuilder"/> instance for chaining.</returns>
43+
public MultiProviderBuilder AddProvider<TProvider>(string name, Func<IServiceProvider, TProvider>? factory = null)
44+
where TProvider : FeatureProvider
45+
{
46+
if (string.IsNullOrWhiteSpace(name))
47+
{
48+
throw new ArgumentException("Provider name cannot be null or empty.", nameof(name));
49+
}
50+
51+
this._providerFactories.Add(sp =>
52+
{
53+
var provider = factory != null
54+
? factory(sp)
55+
: sp.GetRequiredService<TProvider>();
56+
return new ProviderEntry(provider, name);
57+
});
58+
59+
return this;
60+
}
61+
62+
/// <summary>
63+
/// Adds a provider instance to the multi-provider configuration.
64+
/// </summary>
65+
/// <param name="name">The name for the provider.</param>
66+
/// <param name="provider">The provider instance to add.</param>
67+
/// <returns>The <see cref="MultiProviderBuilder"/> instance for chaining.</returns>
68+
public MultiProviderBuilder AddProvider(string name, FeatureProvider provider)
69+
{
70+
if (string.IsNullOrWhiteSpace(name))
71+
{
72+
throw new ArgumentException("Provider name cannot be null or empty.", nameof(name));
73+
}
74+
75+
if (provider == null)
76+
{
77+
throw new ArgumentNullException(nameof(provider), "Provider configuration cannot be null.");
78+
}
79+
80+
return AddProvider<FeatureProvider>(name, _ => provider);
81+
}
82+
83+
/// <summary>
84+
/// Sets the evaluation strategy for the multi-provider.
85+
/// </summary>
86+
/// <typeparam name="TStrategy">The type of the evaluation strategy.</typeparam>
87+
/// <returns>The <see cref="MultiProviderBuilder"/> instance for chaining.</returns>
88+
public MultiProviderBuilder UseStrategy<TStrategy>()
89+
where TStrategy : BaseEvaluationStrategy, new()
90+
{
91+
return UseStrategy(static _ => new TStrategy());
92+
}
93+
94+
/// <summary>
95+
/// Sets the evaluation strategy for the multi-provider using a factory method.
96+
/// </summary>
97+
/// <param name="factory">A factory method to create the strategy instance.</param>
98+
/// <returns>The <see cref="MultiProviderBuilder"/> instance for chaining.</returns>
99+
public MultiProviderBuilder UseStrategy(Func<IServiceProvider, BaseEvaluationStrategy> factory)
100+
{
101+
this._strategyFactory = factory ?? throw new ArgumentNullException(nameof(factory), "Strategy for multi-provider cannot be null.");
102+
return this;
103+
}
104+
105+
/// <summary>
106+
/// Sets the evaluation strategy for the multi-provider.
107+
/// </summary>
108+
/// <param name="strategy">The strategy instance to use.</param>
109+
/// <returns>The <see cref="MultiProviderBuilder"/> instance for chaining.</returns>
110+
public MultiProviderBuilder UseStrategy(BaseEvaluationStrategy strategy)
111+
{
112+
if (strategy == null)
113+
{
114+
throw new ArgumentNullException(nameof(strategy));
115+
}
116+
117+
return UseStrategy(_ => strategy);
118+
}
119+
120+
/// <summary>
121+
/// Builds the provider entries using the service provider.
122+
/// </summary>
123+
internal List<ProviderEntry> BuildProviderEntries(IServiceProvider serviceProvider)
124+
{
125+
return this._providerFactories.Select(factory => factory(serviceProvider)).ToList();
126+
}
127+
128+
/// <summary>
129+
/// Builds the evaluation strategy using the service provider.
130+
/// </summary>
131+
internal BaseEvaluationStrategy? BuildEvaluationStrategy(IServiceProvider serviceProvider)
132+
{
133+
return this._strategyFactory?.Invoke(serviceProvider);
134+
}
135+
}
Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<RootNamespace>OpenFeature.Providers.MultiProvider</RootNamespace>
5-
<PackageReadmeFile>README.md</PackageReadmeFile>
6-
</PropertyGroup>
3+
<PropertyGroup>
4+
<RootNamespace>OpenFeature.Providers.MultiProvider</RootNamespace>
5+
<PackageReadmeFile>README.md</PackageReadmeFile>
6+
</PropertyGroup>
77

8-
<ItemGroup>
9-
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
10-
<InternalsVisibleTo Include="OpenFeature.Providers.MultiProvider.Tests" />
11-
<None Include="README.md" Pack="true" PackagePath="/" />
12-
</ItemGroup>
8+
<ItemGroup>
9+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
10+
<InternalsVisibleTo Include="OpenFeature.Providers.MultiProvider.Tests" />
11+
<None Include="README.md" Pack="true" PackagePath="/" />
12+
</ItemGroup>
1313

14-
<ItemGroup>
15-
<ProjectReference Include="..\OpenFeature\OpenFeature.csproj" />
16-
</ItemGroup>
14+
<ItemGroup>
15+
<ProjectReference Include="..\OpenFeature\OpenFeature.csproj" />
16+
<ProjectReference Include="..\OpenFeature.Hosting\OpenFeature.Hosting.csproj" />
17+
</ItemGroup>
1718
</Project>

0 commit comments

Comments
 (0)