Skip to content

Commit 7237053

Browse files
authored
feat: Add multi-provider support (#488)
* feat: implement MultiProvider class with placeholder methods for feature resolution Signed-off-by: André Silva <[email protected]> * feat: add BaseEvaluationStrategy and MultiProvider classes for multi-provider feature flag evaluation Signed-off-by: André Silva <[email protected]> * feat: implement MultiProvider methods for feature flag resolution using evaluation strategy Signed-off-by: André Silva <[email protected]> * feat: add ComparisonStrategy, FirstMatchStrategy, and FirstSuccessfulStrategy classes for feature flag evaluation Signed-off-by: André Silva <[email protected]> * feat: implement EvaluateAsync method in FirstMatchStrategy for type-specific feature resolution Signed-off-by: André Silva <[email protected]> * feat: enhance error handling in FirstMatchStrategy for feature flag resolution Signed-off-by: André Silva <[email protected]> * feat: implement EvaluateAsync method in FirstSuccessfulStrategy for multi-type feature resolution Signed-off-by: André Silva <[email protected]> * feat: refactor feature resolution strategies to use EvaluateAsync method for improved multi-provider support Signed-off-by: André Silva <[email protected]> * Removed ComparisonStrategy.cs Signed-off-by: André Silva <[email protected]> * feat: add unit tests for FirstMatchStrategy and FirstSuccessfulStrategy to enhance multi-provider support Signed-off-by: André Silva <[email protected]> * feat: add unit tests for FirstSuccessfulStrategy to validate multi-provider evaluation logic Signed-off-by: André Silva <[email protected]> * feat: add unit tests for MultiProvider and ProviderExtensions to validate multi-provider functionality Signed-off-by: André Silva <[email protected]> * feat: add unit tests for MultiProvider to validate functionality and strategy delegation Signed-off-by: André Silva <[email protected]> * fix: update GetMetadata method to return non-nullable Metadata type Signed-off-by: André Silva <[email protected]> * feat: implement ShutdownAsync method to gracefully shut down all providers Signed-off-by: André Silva <[email protected]> * feat: implement InitializeAsync method to initialize all providers Signed-off-by: André Silva <[email protected]> * Move to Extensions folder Signed-off-by: André Silva <[email protected]> * test: add initialization and shutdown tests for MultiProvider Signed-off-by: André Silva <[email protected]> * fix: enhance ShutdownAsync to handle exceptions from multiple providers Signed-off-by: André Silva <[email protected]> * feat: implement ComparisonStrategy for evaluating provider values with fallback and mismatch handling Signed-off-by: André Silva <[email protected]> * feat: add constructor to MultiProvider for default evaluation strategy Signed-off-by: André Silva <[email protected]> * refactor: update ComparisonStrategy and MultiProviderTests for improved clarity and consistency Signed-off-by: André Silva <[email protected]> * refactor: rename namespaces from OpenFeature.Extensions.MultiProvider to OpenFeature.Providers.MultiProvider Signed-off-by: André Silva <[email protected]> * Removed old files Signed-off-by: André Silva <[email protected]> * feat: add multi-provider support with evaluation strategies Signed-off-by: André Silva <[email protected]> * Revert "Move to Extensions folder" This reverts commit 9ffd149. Signed-off-by: André Silva <[email protected]> * refactor: use 'this' keyword for clarity in constructors across multiple models Signed-off-by: André Silva <[email protected]> * fix: add missing space in ProviderStatus exception handling for consistency Signed-off-by: André Silva <[email protected]> * feat: enhance ProviderResolutionResult to include exception details in resolution results Signed-off-by: André Silva <[email protected]> * feat: add error collection method and refine ProviderError to use Exception type Signed-off-by: André Silva <[email protected]> * refactor: simplify error handling in FirstMatchStrategy and FirstSuccessfulStrategy Signed-off-by: André Silva <[email protected]> * test: add unit tests for FirstSuccessfulStrategy behavior and result determination Signed-off-by: André Silva <[email protected]> * test: add unit tests for FirstMatchStrategy behavior and result determination Signed-off-by: André Silva <[email protected]> * test: simplify tests Signed-off-by: André Silva <[email protected]> * test: add unit tests for ComparisonStrategy RunMode behavior Signed-off-by: André Silva <[email protected]> * test: add unit tests for ComparisonStrategy functionality Signed-off-by: André Silva <[email protected]> * test: add unit tests for ProviderEntry, ProviderStatus, and RegisteredProvider classes Signed-off-by: André Silva <[email protected]> * test: add unit tests for FinalResult and ProviderError classes Signed-off-by: André Silva <[email protected]> * test: add unit tests for ProviderExtensions functionality Signed-off-by: André Silva <[email protected]> * test: add unit tests for MultiProvider functionality Signed-off-by: André Silva <[email protected]> * test: add unit tests for BaseEvaluationStrategy functionality Signed-off-by: André Silva <[email protected]> * test: add multi-provider endpoint and evaluation logic Signed-off-by: André Silva <[email protected]> * refactor: update RegisteredProvider to use internal access modifiers and enhance status management; add unit test for SetStatus method Signed-off-by: André Silva <[email protected]> * docs: add Multi-Provider section to README with usage examples and evaluation strategies Signed-off-by: André Silva <[email protected]> * refactor: change properties in StrategyPerProviderContext to use read-only accessors Signed-off-by: André Silva <[email protected]> * docs: update summary comments in FirstMatchStrategy and FirstSuccessfulStrategy to clarify provider evaluation order Signed-off-by: André Silva <[email protected]> * Refactor StrategyEvaluationContext to use generic types Signed-off-by: André Silva <[email protected]> * refactor: simplify flag resolution logic in EvaluateAsync method Signed-off-by: André Silva <[email protected]> * refactor: replace hardcoded provider name with constant in MultiProvider strategies Signed-off-by: André Silva <[email protected]> * refactor: Rename Exception property to Error in ProviderStatus class Signed-off-by: André Silva <[email protected]> * feat: Improved the thread safety for Multiprovider. Signed-off-by: André Silva <[email protected]> * fix: Update exception object name in MultiProvider tests Signed-off-by: André Silva <[email protected]> * fix: Update ObjectDisposedException object name in MultiProvider tests Signed-off-by: André Silva <[email protected]> * fix: Remove volatile modifier from status fields in RegisteredProvider and MultiProvider Signed-off-by: André Silva <[email protected]> * docs: Clarify evaluation strategy parameter description in MultiProvider constructor Signed-off-by: André Silva <[email protected]> * fix: Update shutdown logic to allow shutdown in Ready or Fatal status and add corresponding tests Signed-off-by: André Silva <[email protected]> * refactor: Move fallback provider resolution logic to a more appropriate location in ComparisonStrategy Signed-off-by: André Silva <[email protected]> * fix: Simplify multi-provider endpoint response and improve error handling Signed-off-by: André Silva <[email protected]> * fix: Improve dispose pattern handling in MultiProvider to ensure correct async initialization and shutdown Signed-off-by: André Silva <[email protected]> * fix: Mark _disposed as volatile to ensure thread-safe access in async methods Signed-off-by: André Silva <[email protected]> * fix: Update MultiProvider to implement IAsyncDisposable and improve dispose pattern handling Signed-off-by: André Silva <[email protected]> --------- Signed-off-by: André Silva <[email protected]>
1 parent 417f3fe commit 7237053

29 files changed

+4572
-1
lines changed

README.md

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ Want to contribute a new sample? See our [CONTRIBUTING](CONTRIBUTING.md) guide!
113113
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
114114
|| [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
115115
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
116-
| 🔬 | [DependencyInjection](#dependency-injection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. |
116+
| 🔬 | [Multi-Provider](#multi-provider) | Use multiple feature flag providers simultaneously with configurable evaluation strategies. |
117+
| 🔬 | [DependencyInjection](#dependency-injection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. |
117118

118119
> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ | Experimental: 🔬
119120

@@ -433,6 +434,96 @@ Hooks support passing per-evaluation data between that stages using `hook data`.
433434
434435
Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
435436
437+
### Multi-Provider
438+
439+
> [!NOTE]
440+
> The Multi-Provider feature is currently experimental. Hooks and events are not supported at the moment.
441+
442+
The Multi-Provider enables the use of multiple underlying feature flag providers simultaneously, allowing different providers to be used for different flag keys or based on specific evaluation strategies.
443+
444+
#### Basic Usage
445+
446+
```csharp
447+
using OpenFeature.Providers.MultiProvider;
448+
using OpenFeature.Providers.MultiProvider.Models;
449+
using OpenFeature.Providers.MultiProvider.Strategies;
450+
451+
// Create provider entries
452+
var providerEntries = new List<ProviderEntry>
453+
{
454+
new(new InMemoryProvider(provider1Flags), "Provider1"),
455+
new(new InMemoryProvider(provider2Flags), "Provider2")
456+
};
457+
458+
// Create multi-provider with FirstMatchStrategy (default)
459+
var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
460+
461+
// Set as the default provider
462+
await Api.Instance.SetProviderAsync(multiProvider);
463+
464+
// Use normally - the multi-provider will handle delegation
465+
var client = Api.Instance.GetClient();
466+
var flagValue = await client.GetBooleanValueAsync("my-flag", false);
467+
```
468+
469+
#### Evaluation Strategies
470+
471+
The Multi-Provider supports different evaluation strategies that determine how multiple providers are used:
472+
473+
##### FirstMatchStrategy (Default)
474+
475+
Evaluates providers sequentially and returns the first result that is not "flag not found". If any provider returns an error, that error is returned immediately.
476+
477+
```csharp
478+
var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
479+
```
480+
481+
##### FirstSuccessfulStrategy
482+
483+
Evaluates providers sequentially and returns the first successful result, ignoring errors. Only if all providers fail will errors be returned.
484+
485+
```csharp
486+
var multiProvider = new MultiProvider(providerEntries, new FirstSuccessfulStrategy());
487+
```
488+
489+
##### ComparisonStrategy
490+
491+
Evaluates all providers in parallel and compares results. If values agree, returns the agreed value. If they disagree, returns the fallback provider's value (or first provider if no fallback is specified) and optionally calls a mismatch callback.
492+
493+
```csharp
494+
// Basic comparison
495+
var multiProvider = new MultiProvider(providerEntries, new ComparisonStrategy());
496+
497+
// With fallback provider
498+
var multiProvider = new MultiProvider(providerEntries,
499+
new ComparisonStrategy(fallbackProvider: provider1));
500+
501+
// With mismatch callback
502+
var multiProvider = new MultiProvider(providerEntries,
503+
new ComparisonStrategy(onMismatch: (mismatchDetails) => {
504+
// Log or handle mismatches between providers
505+
foreach (var kvp in mismatchDetails)
506+
{
507+
Console.WriteLine($"Provider {kvp.Key}: {kvp.Value}");
508+
}
509+
}));
510+
```
511+
512+
#### Evaluation Modes
513+
514+
The Multi-Provider supports two evaluation modes:
515+
516+
- **Sequential**: Providers are evaluated one after another (used by `FirstMatchStrategy` and `FirstSuccessfulStrategy`)
517+
- **Parallel**: All providers are evaluated simultaneously (used by `ComparisonStrategy`)
518+
519+
#### Limitations
520+
521+
- **Hooks are not supported**: Multi-Provider does not currently support hook registration or execution
522+
- **Events are not supported**: Provider events are not propagated from underlying providers
523+
- **Experimental status**: The API may change in future releases
524+
525+
For a complete example, see the [AspNetCore sample](./samples/AspNetCore/README.md) which demonstrates Multi-Provider usage.
526+
436527
### Dependency Injection
437528
438529
> [!NOTE]

samples/AspNetCore/Program.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
using OpenFeature.Hooks;
77
using OpenFeature.Model;
88
using OpenFeature.Providers.Memory;
9+
using OpenFeature.Providers.MultiProvider;
10+
using OpenFeature.Providers.MultiProvider.Models;
11+
using OpenFeature.Providers.MultiProvider.Strategies;
912
using OpenTelemetry.Metrics;
1013
using OpenTelemetry.Resources;
1114
using OpenTelemetry.Trace;
@@ -75,6 +78,7 @@
7578

7679
return TypedResults.Ok("Hello world!");
7780
});
81+
7882
app.MapGet("/test-config", async ([FromServices] IFeatureClient featureClient) =>
7983
{
8084
var testConfigValue = await featureClient.GetObjectValueAsync("test-config",
@@ -85,6 +89,56 @@
8589
return Results.Ok(config);
8690
});
8791

92+
app.MapGet("/multi-provider", async () =>
93+
{
94+
// Create first in-memory provider with some flags
95+
var provider1Flags = new Dictionary<string, Flag>
96+
{
97+
{ "providername", new Flag<string>(new Dictionary<string, string> { { "enabled", "enabled-provider1" }, { "disabled", "disabled-provider1" } }, "enabled") },
98+
{ "max-items", new Flag<int>(new Dictionary<string, int> { { "low", 10 }, { "high", 100 } }, "high") },
99+
};
100+
var provider1 = new InMemoryProvider(provider1Flags);
101+
102+
// Create second in-memory provider with different flags
103+
var provider2Flags = new Dictionary<string, Flag>
104+
{
105+
{ "providername", new Flag<string>(new Dictionary<string, string> { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") },
106+
};
107+
var provider2 = new InMemoryProvider(provider2Flags);
108+
109+
// Create provider entries
110+
var providerEntries = new List<ProviderEntry>
111+
{
112+
new(provider1, "Provider1"),
113+
new(provider2, "Provider2")
114+
};
115+
116+
// Create multi-provider with FirstMatchStrategy (default)
117+
var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
118+
119+
// Set the multi-provider as the default provider using OpenFeature API
120+
await Api.Instance.SetProviderAsync(multiProvider);
121+
122+
// Create a client directly using the API
123+
var client = Api.Instance.GetClient();
124+
125+
try
126+
{
127+
// Test flag evaluation from different providers
128+
var maxItemsFlag = await client.GetIntegerDetailsAsync("max-items", 0);
129+
var providerNameFlag = await client.GetStringDetailsAsync("providername", "default");
130+
131+
// Test a flag that doesn't exist in any provider
132+
var unknownFlag = await client.GetBooleanDetailsAsync("unknown-flag", false);
133+
134+
return Results.Ok();
135+
}
136+
catch (Exception)
137+
{
138+
return Results.InternalServerError();
139+
}
140+
});
141+
88142
app.Run();
89143

90144

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace OpenFeature.Providers.MultiProvider.Models;
2+
3+
internal class ChildProviderStatus
4+
{
5+
public string ProviderName { get; set; } = string.Empty;
6+
public Exception? Error { get; set; }
7+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace OpenFeature.Providers.MultiProvider.Models;
2+
3+
/// <summary>
4+
/// Represents an entry for a provider in the multi-provider configuration.
5+
/// </summary>
6+
public class ProviderEntry
7+
{
8+
/// <summary>
9+
/// Initializes a new instance of the <see cref="ProviderEntry"/> class.
10+
/// </summary>
11+
/// <param name="provider">The feature provider instance.</param>
12+
/// <param name="name">Optional custom name for the provider. If not provided, the provider's metadata name will be used.</param>
13+
public ProviderEntry(FeatureProvider provider, string? name = null)
14+
{
15+
this.Provider = provider ?? throw new ArgumentNullException(nameof(provider));
16+
this.Name = name;
17+
}
18+
19+
/// <summary>
20+
/// Gets the feature provider instance.
21+
/// </summary>
22+
public FeatureProvider Provider { get; }
23+
24+
/// <summary>
25+
/// Gets the optional custom name for the provider.
26+
/// If null, the provider's metadata name should be used.
27+
/// </summary>
28+
public string? Name { get; }
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace OpenFeature.Providers.MultiProvider.Models;
2+
3+
internal class RegisteredProvider
4+
{
5+
#if NET9_0_OR_GREATER
6+
private readonly Lock _statusLock = new();
7+
#else
8+
private readonly object _statusLock = new object();
9+
#endif
10+
11+
private Constant.ProviderStatus _status = Constant.ProviderStatus.NotReady;
12+
13+
internal RegisteredProvider(FeatureProvider provider, string name)
14+
{
15+
this.Provider = provider ?? throw new ArgumentNullException(nameof(provider));
16+
this.Name = name ?? throw new ArgumentNullException(nameof(name));
17+
}
18+
19+
internal FeatureProvider Provider { get; }
20+
21+
internal string Name { get; }
22+
23+
internal Constant.ProviderStatus Status
24+
{
25+
get
26+
{
27+
lock (this._statusLock)
28+
{
29+
return this._status;
30+
}
31+
}
32+
}
33+
34+
internal void SetStatus(Constant.ProviderStatus status)
35+
{
36+
lock (this._statusLock)
37+
{
38+
this._status = status;
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)