Skip to content

Commit 95ae7f0

Browse files
authored
feat: Implement hooks in multi provider (#594)
* Adding GetProviderHooks override. Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Enhance provider evaluation with hook execution support Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Update README to clarify Multi-Provider support for hooks and events Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Replace null with ClientMetadata in EvaluateAsync calls Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * refactor: Require ILogger parameter in EvaluateAsync and related methods Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * test: Add unit test for EvaluateAsync with provider hooks and error handling Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Fix formatting Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * test: Add unit tests for GetProviderHooks and EvaluateAsync with hooks handling Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * add unit test for GetFlagValueType to validate flag value types Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * refactor: remove GetProviderHooks implementation and update related tests to return empty list Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * test: Add unit test for EvaluateAsync to handle exceptions from after hooks Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Update README.md Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
1 parent 51aefbc commit 95ae7f0

File tree

5 files changed

+538
-27
lines changed

5 files changed

+538
-27
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,10 +443,12 @@ Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/
443443
### Multi-Provider
444444
445445
> [!NOTE]
446-
> The Multi-Provider feature is currently experimental. Hooks and events are not supported at the moment.
446+
> The Multi-Provider feature is currently experimental.
447447
448448
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.
449449
450+
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.
451+
450452
#### Basic Usage
451453
452454
```csharp
@@ -524,9 +526,7 @@ The Multi-Provider supports two evaluation modes:
524526
525527
#### Limitations
526528
527-
- **Hooks are not supported**: Multi-Provider does not currently support hook registration or execution
528-
- **Events are not supported**: Provider events are not propagated from underlying providers
529-
- **Experimental status**: The API may change in future releases
529+
- **Experimental status**: The API may change in future releases
530530
531531
For a complete example, see the [AspNetCore sample](./samples/AspNetCore/README.md) which demonstrates Multi-Provider usage.
532532

src/OpenFeature.Providers.MultiProvider/MultiProvider.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ private async Task<List<ProviderResolutionResult<T>>> SequentialEvaluationAsync<
270270
continue;
271271
}
272272

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

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

298298
if (this._evaluationStrategy.ShouldEvaluateThisProvider(providerContext, evaluationContext))
299299
{
300-
tasks.Add(registeredProvider.Provider.EvaluateAsync(providerContext, evaluationContext, defaultValue, cancellationToken));
300+
tasks.Add(registeredProvider.Provider.EvaluateAsync(providerContext, evaluationContext, defaultValue, this._logger, cancellationToken));
301301
}
302302
}
303303

src/OpenFeature.Providers.MultiProvider/ProviderExtensions.cs

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.Extensions.Logging;
13
using OpenFeature.Constant;
4+
using OpenFeature.Error;
5+
using OpenFeature.Extension;
26
using OpenFeature.Model;
37
using OpenFeature.Providers.MultiProvider.Strategies.Models;
48

@@ -11,23 +15,56 @@ internal static async Task<ProviderResolutionResult<T>> EvaluateAsync<T>(
1115
StrategyPerProviderContext<T> providerContext,
1216
EvaluationContext? evaluationContext,
1317
T defaultValue,
14-
CancellationToken cancellationToken)
18+
ILogger logger,
19+
CancellationToken cancellationToken = default)
1520
{
1621
var key = providerContext.FlagKey;
1722

1823
try
1924
{
25+
// Execute provider hooks for this specific provider
26+
var providerHooks = provider.GetProviderHooks();
27+
EvaluationContext? contextForThisProvider = evaluationContext;
28+
29+
if (providerHooks.Count > 0)
30+
{
31+
// Execute hooks for this provider with context isolation
32+
var (modifiedContext, hookResult) = await ExecuteBeforeEvaluationHooksAsync(
33+
provider,
34+
providerHooks,
35+
key,
36+
defaultValue,
37+
evaluationContext,
38+
logger,
39+
cancellationToken).ConfigureAwait(false);
40+
41+
if (hookResult != null)
42+
{
43+
return hookResult;
44+
}
45+
46+
contextForThisProvider = modifiedContext ?? evaluationContext;
47+
}
48+
49+
// Evaluate the flag with the (possibly modified) context
2050
var result = defaultValue switch
2151
{
22-
bool boolDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveBooleanValueAsync(key, boolDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
23-
string stringDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, stringDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
24-
int intDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveIntegerValueAsync(key, intDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
25-
double doubleDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveDoubleValueAsync(key, doubleDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
26-
Value valueDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, valueDefaultValue, evaluationContext, cancellationToken).ConfigureAwait(false),
27-
null when typeof(T) == typeof(string) => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
28-
null when typeof(T) == typeof(Value) => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
52+
bool boolDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveBooleanValueAsync(key, boolDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
53+
string stringDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, stringDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
54+
int intDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveIntegerValueAsync(key, intDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
55+
double doubleDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveDoubleValueAsync(key, doubleDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
56+
Value valueDefaultValue => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, valueDefaultValue, contextForThisProvider, cancellationToken).ConfigureAwait(false),
57+
null when typeof(T) == typeof(string) => (ResolutionDetails<T>)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, contextForThisProvider, cancellationToken).ConfigureAwait(false),
58+
null when typeof(T) == typeof(Value) => (ResolutionDetails<T>)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, contextForThisProvider, cancellationToken).ConfigureAwait(false),
2959
_ => throw new ArgumentException($"Unsupported flag type: {typeof(T)}")
3060
};
61+
62+
// Execute after/finally hooks for this provider if we have them
63+
if (providerHooks.Count > 0)
64+
{
65+
await ExecuteAfterEvaluationHooksAsync(provider, providerHooks, key, defaultValue, contextForThisProvider, result, logger, cancellationToken).ConfigureAwait(false);
66+
}
67+
3168
return new ProviderResolutionResult<T>(provider, providerContext.ProviderName, result);
3269
}
3370
catch (Exception ex)
@@ -43,4 +80,101 @@ null when typeof(T) == typeof(Value) => (ResolutionDetails<T>)(object)await prov
4380
return new ProviderResolutionResult<T>(provider, providerContext.ProviderName, errorResult, ex);
4481
}
4582
}
83+
84+
private static async Task<(EvaluationContext?, ProviderResolutionResult<T>?)> ExecuteBeforeEvaluationHooksAsync<T>(
85+
FeatureProvider provider,
86+
IImmutableList<Hook> hooks,
87+
string key,
88+
T defaultValue,
89+
EvaluationContext? evaluationContext,
90+
ILogger logger,
91+
CancellationToken cancellationToken)
92+
{
93+
try
94+
{
95+
var sharedHookContext = new SharedHookContext<T>(
96+
key,
97+
defaultValue,
98+
GetFlagValueType<T>(),
99+
new ClientMetadata(MultiProviderConstants.ProviderName, null),
100+
provider.GetMetadata()
101+
);
102+
103+
var initialContext = evaluationContext ?? EvaluationContext.Empty;
104+
var hookRunner = new HookRunner<T>([.. hooks], initialContext, sharedHookContext, logger);
105+
106+
// Execute before hooks for this provider
107+
var modifiedContext = await hookRunner.TriggerBeforeHooksAsync(null, cancellationToken).ConfigureAwait(false);
108+
return (modifiedContext, null);
109+
}
110+
catch (Exception hookEx)
111+
{
112+
// If before hooks fail, return error result
113+
var errorResult = new ResolutionDetails<T>(
114+
key,
115+
defaultValue,
116+
ErrorType.General,
117+
Reason.Error,
118+
errorMessage: $"Provider hook execution failed: {hookEx.Message}");
119+
120+
var result = new ProviderResolutionResult<T>(provider, provider.GetMetadata()?.Name ?? "unknown", errorResult, hookEx);
121+
return (null, result);
122+
}
123+
}
124+
125+
private static async Task ExecuteAfterEvaluationHooksAsync<T>(
126+
FeatureProvider provider,
127+
IImmutableList<Hook> hooks,
128+
string key,
129+
T defaultValue,
130+
EvaluationContext? evaluationContext,
131+
ResolutionDetails<T> result,
132+
ILogger logger,
133+
CancellationToken cancellationToken)
134+
{
135+
try
136+
{
137+
var sharedHookContext = new SharedHookContext<T>(
138+
key,
139+
defaultValue,
140+
GetFlagValueType<T>(),
141+
new ClientMetadata(MultiProviderConstants.ProviderName, null),
142+
provider.GetMetadata()
143+
);
144+
145+
var hookRunner = new HookRunner<T>([.. hooks], evaluationContext ?? EvaluationContext.Empty, sharedHookContext, logger);
146+
147+
var evaluationDetails = result.ToFlagEvaluationDetails();
148+
149+
if (result.ErrorType == ErrorType.None)
150+
{
151+
await hookRunner.TriggerAfterHooksAsync(evaluationDetails, null, cancellationToken).ConfigureAwait(false);
152+
}
153+
else
154+
{
155+
var exception = new FeatureProviderException(result.ErrorType, result.ErrorMessage);
156+
await hookRunner.TriggerErrorHooksAsync(exception, null, cancellationToken).ConfigureAwait(false);
157+
}
158+
159+
await hookRunner.TriggerFinallyHooksAsync(evaluationDetails, null, cancellationToken).ConfigureAwait(false);
160+
}
161+
catch (Exception hookEx)
162+
{
163+
// Log hook execution errors but don't fail the evaluation
164+
logger.LogWarning(hookEx, "Provider after/finally hook execution failed for provider {ProviderName}", provider.GetMetadata()?.Name ?? "unknown");
165+
}
166+
}
167+
168+
internal static FlagValueType GetFlagValueType<T>()
169+
{
170+
return typeof(T) switch
171+
{
172+
_ when typeof(T) == typeof(bool) => FlagValueType.Boolean,
173+
_ when typeof(T) == typeof(string) => FlagValueType.String,
174+
_ when typeof(T) == typeof(int) => FlagValueType.Number,
175+
_ when typeof(T) == typeof(double) => FlagValueType.Number,
176+
_ when typeof(T) == typeof(Value) => FlagValueType.Object,
177+
_ => FlagValueType.Object // Default fallback
178+
};
179+
}
46180
}

0 commit comments

Comments
 (0)