Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
af8c70a
feat: implement MultiProvider class with placeholder methods for feat…
askpt Jun 5, 2025
6467b75
feat: add BaseEvaluationStrategy and MultiProvider classes for multi-…
askpt Jun 5, 2025
0a69cee
feat: implement MultiProvider methods for feature flag resolution usi…
askpt Jun 5, 2025
0f00aa5
feat: add ComparisonStrategy, FirstMatchStrategy, and FirstSuccessful…
askpt Jun 6, 2025
d743d2d
feat: implement EvaluateAsync method in FirstMatchStrategy for type-s…
askpt Jun 6, 2025
9e7ed97
feat: enhance error handling in FirstMatchStrategy for feature flag r…
askpt Jun 6, 2025
ddd714e
feat: implement EvaluateAsync method in FirstSuccessfulStrategy for m…
askpt Jun 6, 2025
fdb2e63
feat: refactor feature resolution strategies to use EvaluateAsync met…
askpt Jun 6, 2025
776aa61
Removed ComparisonStrategy.cs
askpt Jun 6, 2025
30f8c94
feat: add unit tests for FirstMatchStrategy and FirstSuccessfulStrate…
askpt Jun 6, 2025
b12a3cb
feat: add unit tests for FirstSuccessfulStrategy to validate multi-pr…
askpt Jun 6, 2025
a258469
feat: add unit tests for MultiProvider and ProviderExtensions to vali…
askpt Jun 6, 2025
8f75270
feat: add unit tests for MultiProvider to validate functionality and …
askpt Jun 6, 2025
aca836f
fix: update GetMetadata method to return non-nullable Metadata type
askpt Jun 6, 2025
df11573
feat: implement ShutdownAsync method to gracefully shut down all prov…
askpt Jun 6, 2025
6054102
feat: implement InitializeAsync method to initialize all providers
askpt Jun 6, 2025
a38e126
Merge branch 'main' into askpt/487-add-multi-provider-support
askpt Jun 28, 2025
9ffd149
Move to Extensions folder
askpt Jun 28, 2025
8f8127d
test: add initialization and shutdown tests for MultiProvider
askpt Jun 28, 2025
dccb2c2
fix: enhance ShutdownAsync to handle exceptions from multiple providers
askpt Jun 28, 2025
8ce39b2
feat: implement ComparisonStrategy for evaluating provider values wit…
askpt Jun 28, 2025
1830676
feat: add constructor to MultiProvider for default evaluation strategy
askpt Jun 28, 2025
b9bd51e
refactor: update ComparisonStrategy and MultiProviderTests for improv…
askpt Jun 28, 2025
1149ac7
refactor: rename namespaces from OpenFeature.Extensions.MultiProvider…
askpt Jun 28, 2025
35b07b3
Removed old files
askpt Jun 28, 2025
bfb79c4
feat: add multi-provider support with evaluation strategies
askpt Jun 28, 2025
0e7f202
Revert "Move to Extensions folder"
askpt Jun 28, 2025
ecab884
refactor: use 'this' keyword for clarity in constructors across multi…
askpt Jun 29, 2025
05f779c
fix: add missing space in ProviderStatus exception handling for consi…
askpt Jun 29, 2025
822d983
feat: enhance ProviderResolutionResult to include exception details i…
askpt Jun 29, 2025
e03d85d
feat: add error collection method and refine ProviderError to use Exc…
askpt Jun 29, 2025
644d620
refactor: simplify error handling in FirstMatchStrategy and FirstSucc…
askpt Jun 30, 2025
c96d435
test: add unit tests for FirstSuccessfulStrategy behavior and result …
askpt Jun 30, 2025
b29fe39
test: add unit tests for FirstMatchStrategy behavior and result deter…
askpt Jun 30, 2025
9c3a213
test: simplify tests
askpt Jun 30, 2025
2255016
test: add unit tests for ComparisonStrategy RunMode behavior
askpt Jun 30, 2025
d6c6e1e
test: add unit tests for ComparisonStrategy functionality
askpt Jun 30, 2025
dedadc8
test: add unit tests for ProviderEntry, ProviderStatus, and Registere…
askpt Jun 30, 2025
1ced81c
test: add unit tests for FinalResult and ProviderError classes
askpt Jun 30, 2025
af70ace
test: add unit tests for ProviderExtensions functionality
askpt Jun 30, 2025
be942fb
test: add unit tests for MultiProvider functionality
askpt Jul 4, 2025
3a325ed
test: add unit tests for BaseEvaluationStrategy functionality
askpt Jul 4, 2025
c8570fd
test: add multi-provider endpoint and evaluation logic
askpt Jul 7, 2025
ac5d076
refactor: update RegisteredProvider to use internal access modifiers …
askpt Jul 7, 2025
099b75d
docs: add Multi-Provider section to README with usage examples and ev…
askpt Jul 7, 2025
e4fbcf5
Merge branch 'main' into askpt/487-add-multi-provider-support
askpt Jul 7, 2025
bbdf85d
refactor: change properties in StrategyPerProviderContext to use read…
askpt Jul 12, 2025
19fd589
docs: update summary comments in FirstMatchStrategy and FirstSuccessf…
askpt Jul 12, 2025
1e293cd
Refactor StrategyEvaluationContext to use generic types
askpt Jul 12, 2025
e48b6bd
refactor: simplify flag resolution logic in EvaluateAsync method
askpt Jul 12, 2025
828d564
refactor: replace hardcoded provider name with constant in MultiProvi…
askpt Jul 12, 2025
f124dce
Merge branch 'main' into askpt/487-add-multi-provider-support
askpt Jul 13, 2025
2b8a682
Merge branch 'main' into askpt/487-add-multi-provider-support
askpt Jul 21, 2025
85ef2b3
Merge branch 'main' into askpt/487-add-multi-provider-support
askpt Jul 28, 2025
dac15c6
refactor: Rename Exception property to Error in ProviderStatus class
askpt Jul 28, 2025
339b196
feat: Improved the thread safety for Multiprovider.
askpt Jul 28, 2025
7637aee
fix: Update exception object name in MultiProvider tests
askpt Jul 28, 2025
78acd86
fix: Update ObjectDisposedException object name in MultiProvider tests
askpt Jul 28, 2025
389e85a
Merge branch 'main' into askpt/487-add-multi-provider-support
askpt Jul 28, 2025
6685f01
fix: Remove volatile modifier from status fields in RegisteredProvide…
askpt Jul 29, 2025
036d23d
docs: Clarify evaluation strategy parameter description in MultiProvi…
askpt Jul 31, 2025
ed784d4
fix: Update shutdown logic to allow shutdown in Ready or Fatal status…
askpt Jul 31, 2025
8d1bb37
refactor: Move fallback provider resolution logic to a more appropria…
askpt Jul 31, 2025
62641a0
Merge branch 'main' into askpt/487-add-multi-provider-support
askpt Jul 31, 2025
340df6e
fix: Simplify multi-provider endpoint response and improve error hand…
askpt Jul 31, 2025
eb98a1b
fix: Improve dispose pattern handling in MultiProvider to ensure corr…
askpt Aug 1, 2025
003e343
fix: Mark _disposed as volatile to ensure thread-safe access in async…
askpt Aug 4, 2025
bfeab28
fix: Update MultiProvider to implement IAsyncDisposable and improve d…
askpt Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ Want to contribute a new sample? See our [CONTRIBUTING](CONTRIBUTING.md) guide!
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ✅ | [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). |
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
| 🔬 | [DependencyInjection](#dependency-injection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. |
| 🔬 | [Multi-Provider](#multi-provider) | Use multiple feature flag providers simultaneously with configurable evaluation strategies. |
| 🔬 | [DependencyInjection](#dependency-injection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. |

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

Expand Down Expand Up @@ -433,6 +434,96 @@ Hooks support passing per-evaluation data between that stages using `hook data`.

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!

### Multi-Provider

> [!NOTE]
> The Multi-Provider feature is currently experimental. Hooks and events are not supported at the moment.

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.

#### Basic Usage

```csharp
using OpenFeature.Providers.MultiProvider;
using OpenFeature.Providers.MultiProvider.Models;
using OpenFeature.Providers.MultiProvider.Strategies;

// Create provider entries
var providerEntries = new List<ProviderEntry>
{
new(new InMemoryProvider(provider1Flags), "Provider1"),
new(new InMemoryProvider(provider2Flags), "Provider2")
};

// Create multi-provider with FirstMatchStrategy (default)
var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());

// Set as the default provider
await Api.Instance.SetProviderAsync(multiProvider);

// Use normally - the multi-provider will handle delegation
var client = Api.Instance.GetClient();
var flagValue = await client.GetBooleanValueAsync("my-flag", false);
```

#### Evaluation Strategies

The Multi-Provider supports different evaluation strategies that determine how multiple providers are used:

##### FirstMatchStrategy (Default)

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.

```csharp
var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
```

##### FirstSuccessfulStrategy

Evaluates providers sequentially and returns the first successful result, ignoring errors. Only if all providers fail will errors be returned.

```csharp
var multiProvider = new MultiProvider(providerEntries, new FirstSuccessfulStrategy());
```

##### ComparisonStrategy

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.

```csharp
// Basic comparison
var multiProvider = new MultiProvider(providerEntries, new ComparisonStrategy());

// With fallback provider
var multiProvider = new MultiProvider(providerEntries,
new ComparisonStrategy(fallbackProvider: provider1));

// With mismatch callback
var multiProvider = new MultiProvider(providerEntries,
new ComparisonStrategy(onMismatch: (mismatchDetails) => {
// Log or handle mismatches between providers
foreach (var kvp in mismatchDetails)
{
Console.WriteLine($"Provider {kvp.Key}: {kvp.Value}");
}
}));
```

#### Evaluation Modes

The Multi-Provider supports two evaluation modes:

- **Sequential**: Providers are evaluated one after another (used by `FirstMatchStrategy` and `FirstSuccessfulStrategy`)
- **Parallel**: All providers are evaluated simultaneously (used by `ComparisonStrategy`)

#### Limitations

- **Hooks are not supported**: Multi-Provider does not currently support hook registration or execution
- **Events are not supported**: Provider events are not propagated from underlying providers
- **Experimental status**: The API may change in future releases

For a complete example, see the [AspNetCore sample](./samples/AspNetCore/README.md) which demonstrates Multi-Provider usage.

### Dependency Injection

> [!NOTE]
Expand Down
54 changes: 54 additions & 0 deletions samples/AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
using OpenFeature.Hooks;
using OpenFeature.Model;
using OpenFeature.Providers.Memory;
using OpenFeature.Providers.MultiProvider;
using OpenFeature.Providers.MultiProvider.Models;
using OpenFeature.Providers.MultiProvider.Strategies;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
Expand Down Expand Up @@ -75,6 +78,7 @@

return TypedResults.Ok("Hello world!");
});

app.MapGet("/test-config", async ([FromServices] IFeatureClient featureClient) =>
{
var testConfigValue = await featureClient.GetObjectValueAsync("test-config",
Expand All @@ -85,6 +89,56 @@
return Results.Ok(config);
});

app.MapGet("/multi-provider", async () =>
{
// Create first in-memory provider with some flags
var provider1Flags = new Dictionary<string, Flag>
{
{ "providername", new Flag<string>(new Dictionary<string, string> { { "enabled", "enabled-provider1" }, { "disabled", "disabled-provider1" } }, "enabled") },
{ "max-items", new Flag<int>(new Dictionary<string, int> { { "low", 10 }, { "high", 100 } }, "high") },
};
var provider1 = new InMemoryProvider(provider1Flags);

// Create second in-memory provider with different flags
var provider2Flags = new Dictionary<string, Flag>
{
{ "providername", new Flag<string>(new Dictionary<string, string> { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") },
};
var provider2 = new InMemoryProvider(provider2Flags);

// Create provider entries
var providerEntries = new List<ProviderEntry>
{
new(provider1, "Provider1"),
new(provider2, "Provider2")
};

// Create multi-provider with FirstMatchStrategy (default)
var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());

// Set the multi-provider as the default provider using OpenFeature API
await Api.Instance.SetProviderAsync(multiProvider);

// Create a client directly using the API
var client = Api.Instance.GetClient();

try
{
// Test flag evaluation from different providers
var maxItemsFlag = await client.GetIntegerDetailsAsync("max-items", 0);
var providerNameFlag = await client.GetStringDetailsAsync("providername", "default");

// Test a flag that doesn't exist in any provider
var unknownFlag = await client.GetBooleanDetailsAsync("unknown-flag", false);

return Results.Ok();
}
catch (Exception)
{
return Results.InternalServerError();
}
});

app.Run();


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OpenFeature.Providers.MultiProvider.Models;

internal class ChildProviderStatus
{
public string ProviderName { get; set; } = string.Empty;
public Exception? Error { get; set; }
}
29 changes: 29 additions & 0 deletions src/OpenFeature/Providers/MultiProvider/Models/ProviderEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace OpenFeature.Providers.MultiProvider.Models;

/// <summary>
/// Represents an entry for a provider in the multi-provider configuration.
/// </summary>
public class ProviderEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="ProviderEntry"/> class.
/// </summary>
/// <param name="provider">The feature provider instance.</param>
/// <param name="name">Optional custom name for the provider. If not provided, the provider's metadata name will be used.</param>
public ProviderEntry(FeatureProvider provider, string? name = null)
{
this.Provider = provider ?? throw new ArgumentNullException(nameof(provider));
this.Name = name;
}

/// <summary>
/// Gets the feature provider instance.
/// </summary>
public FeatureProvider Provider { get; }

/// <summary>
/// Gets the optional custom name for the provider.
/// If null, the provider's metadata name should be used.
/// </summary>
public string? Name { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace OpenFeature.Providers.MultiProvider.Models;

internal class RegisteredProvider
{
#if NET9_0_OR_GREATER
private readonly Lock _statusLock = new();
#else
private readonly object _statusLock = new object();
#endif

private Constant.ProviderStatus _status = Constant.ProviderStatus.NotReady;

internal RegisteredProvider(FeatureProvider provider, string name)
{
this.Provider = provider ?? throw new ArgumentNullException(nameof(provider));
this.Name = name ?? throw new ArgumentNullException(nameof(name));
}

internal FeatureProvider Provider { get; }

internal string Name { get; }

internal Constant.ProviderStatus Status
{
get
{
lock (this._statusLock)
{
return this._status;
}
}
}

internal void SetStatus(Constant.ProviderStatus status)
{
lock (this._statusLock)
{
this._status = status;
}
}
}
Loading
Loading