Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,10 +443,12 @@ Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/
### Multi-Provider

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

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.

The Multi-Provider supports provider hooks and executes them in accordance with the OpenFeature specification. Each provider's hooks are executed with context isolation, ensuring that context modifications by one provider's hooks do not affect other providers.

#### Basic Usage

```csharp
Expand Down Expand Up @@ -524,9 +526,7 @@ The Multi-Provider supports two evaluation modes:

#### 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
- **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.

Expand Down
4 changes: 2 additions & 2 deletions src/OpenFeature.Providers.MultiProvider/MultiProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ private async Task<List<ProviderResolutionResult<T>>> SequentialEvaluationAsync<
continue;
}

var result = await registeredProvider.Provider.EvaluateAsync(providerContext, evaluationContext, defaultValue, cancellationToken).ConfigureAwait(false);
var result = await registeredProvider.Provider.EvaluateAsync(providerContext, evaluationContext, defaultValue, this._logger, cancellationToken).ConfigureAwait(false);
resolutions.Add(result);

if (!this._evaluationStrategy.ShouldEvaluateNextProvider(providerContext, evaluationContext, result))
Expand All @@ -297,7 +297,7 @@ private async Task<List<ProviderResolutionResult<T>>> ParallelEvaluationAsync<T>

if (this._evaluationStrategy.ShouldEvaluateThisProvider(providerContext, evaluationContext))
{
tasks.Add(registeredProvider.Provider.EvaluateAsync(providerContext, evaluationContext, defaultValue, cancellationToken));
tasks.Add(registeredProvider.Provider.EvaluateAsync(providerContext, evaluationContext, defaultValue, this._logger, cancellationToken));
}
}

Expand Down
150 changes: 142 additions & 8 deletions src/OpenFeature.Providers.MultiProvider/ProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using OpenFeature.Constant;
using OpenFeature.Error;
using OpenFeature.Extension;
using OpenFeature.Model;
using OpenFeature.Providers.MultiProvider.Strategies.Models;

Expand All @@ -11,23 +15,56 @@ internal static async Task<ProviderResolutionResult<T>> EvaluateAsync<T>(
StrategyPerProviderContext<T> providerContext,
EvaluationContext? evaluationContext,
T defaultValue,
CancellationToken cancellationToken)
ILogger logger,
CancellationToken cancellationToken = default)
{
var key = providerContext.FlagKey;

try
{
// Execute provider hooks for this specific provider
var providerHooks = provider.GetProviderHooks();
EvaluationContext? contextForThisProvider = evaluationContext;

if (providerHooks.Count > 0)
{
// Execute hooks for this provider with context isolation
var (modifiedContext, hookResult) = await ExecuteBeforeEvaluationHooksAsync(
provider,
providerHooks,
key,
defaultValue,
evaluationContext,
logger,
cancellationToken).ConfigureAwait(false);

if (hookResult != null)
{
return hookResult;
}

contextForThisProvider = modifiedContext ?? evaluationContext;
}

// Evaluate the flag with the (possibly modified) context
var result = defaultValue switch
{
bool boolDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveBooleanValueAsync(key, boolDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
string stringDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, stringDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
int intDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveIntegerValueAsync(key, intDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
double doubleDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveDoubleValueAsync(key, doubleDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
Value valueDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, valueDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
null when typeof(T) == typeof(string) => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
null when typeof(T) == typeof(Value) => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
bool boolDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveBooleanValueAsync(key, boolDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
string stringDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, stringDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
int intDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveIntegerValueAsync(key, intDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
double doubleDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveDoubleValueAsync(key, doubleDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
Value valueDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, valueDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
null when typeof(T) == typeof(string) => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, contextForThisProvider, cancellationToken).ConfigureAwait(false),
null when typeof(T) == typeof(Value) => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, contextForThisProvider, cancellationToken).ConfigureAwait(false),
_ => throw new ArgumentException($"Unsupported flag type: {typeof(T)}")
};

// Execute after/finally hooks for this provider if we have them
if (providerHooks.Count > 0)
{
await ExecuteAfterEvaluationHooksAsync(provider, providerHooks, key, defaultValue, contextForThisProvider, result, logger, cancellationToken).ConfigureAwait(false);
}

return new ProviderResolutionResult<T>(provider, providerContext.ProviderName, result);
}
catch (Exception ex)
Expand All @@ -43,4 +80,101 @@ null when typeof(T) == typeof(Value) => (ResolutionDetails<T>)(object)await prov
return new ProviderResolutionResult<T>(provider, providerContext.ProviderName, errorResult, ex);
}
}

private static async Task<(EvaluationContext?, ProviderResolutionResult<T>?)> ExecuteBeforeEvaluationHooksAsync<T>(
FeatureProvider provider,
IImmutableList<Hook> hooks,
string key,
T defaultValue,
EvaluationContext? evaluationContext,
ILogger logger,
CancellationToken cancellationToken)
{
try
{
var sharedHookContext = new SharedHookContext<T>(
key,
defaultValue,
GetFlagValueType<T>(),
new ClientMetadata(MultiProviderConstants.ProviderName, null),
provider.GetMetadata()
);

var initialContext = evaluationContext ?? EvaluationContext.Empty;
var hookRunner = new HookRunner<T>([.. hooks], initialContext, sharedHookContext, logger);

// Execute before hooks for this provider
var modifiedContext = await hookRunner.TriggerBeforeHooksAsync(null, cancellationToken).ConfigureAwait(false);
return (modifiedContext, null);
}
catch (Exception hookEx)
{
// If before hooks fail, return error result
var errorResult = new ResolutionDetails<T>(
key,
defaultValue,
ErrorType.General,
Reason.Error,
errorMessage: $"Provider hook execution failed: {hookEx.Message}");

var result = new ProviderResolutionResult<T>(provider, provider.GetMetadata()?.Name ?? "unknown", errorResult, hookEx);
return (null, result);
}
}

private static async Task ExecuteAfterEvaluationHooksAsync<T>(
FeatureProvider provider,
IImmutableList<Hook> hooks,
string key,
T defaultValue,
EvaluationContext? evaluationContext,
ResolutionDetails<T> result,
ILogger logger,
CancellationToken cancellationToken)
{
try
{
var sharedHookContext = new SharedHookContext<T>(
key,
defaultValue,
GetFlagValueType<T>(),
new ClientMetadata(MultiProviderConstants.ProviderName, null),
provider.GetMetadata()
);

var hookRunner = new HookRunner<T>([.. hooks], evaluationContext ?? EvaluationContext.Empty, sharedHookContext, logger);

var evaluationDetails = result.ToFlagEvaluationDetails();

if (result.ErrorType == ErrorType.None)
{
await hookRunner.TriggerAfterHooksAsync(evaluationDetails, null, cancellationToken).ConfigureAwait(false);
}
else
{
var exception = new FeatureProviderException(result.ErrorType, result.ErrorMessage);
await hookRunner.TriggerErrorHooksAsync(exception, null, cancellationToken).ConfigureAwait(false);
}

await hookRunner.TriggerFinallyHooksAsync(evaluationDetails, null, cancellationToken).ConfigureAwait(false);
}
catch (Exception hookEx)
{
// Log hook execution errors but don't fail the evaluation
logger.LogWarning(hookEx, "Provider after/finally hook execution failed for provider {ProviderName}", provider.GetMetadata()?.Name ?? "unknown");
}
}

internal static FlagValueType GetFlagValueType<T>()
{
return typeof(T) switch
{
_ when typeof(T) == typeof(bool) => FlagValueType.Boolean,
_ when typeof(T) == typeof(string) => FlagValueType.String,
_ when typeof(T) == typeof(int) => FlagValueType.Number,
_ when typeof(T) == typeof(double) => FlagValueType.Number,
_ when typeof(T) == typeof(Value) => FlagValueType.Object,
_ => FlagValueType.Object // Default fallback
};
}
}
Loading