Skip to content

Commit 2ef9955

Browse files
authored
feat: Add evaluation details to finally hook stage (#335)
Signed-off-by: André Silva <[email protected]>
1 parent 8527b03 commit 2ef9955

File tree

6 files changed

+80
-46
lines changed

6 files changed

+80
-46
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ public class MyHook : Hook
305305
// code to run if there's an error during before hooks or during flag evaluation
306306
}
307307

308-
public ValueTask FinallyAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object> hints = null)
308+
public ValueTask FinallyAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> evaluationDetails, IReadOnlyDictionary<string, object> hints = null)
309309
{
310310
// code to run after all other stages, regardless of success/failure
311311
}

src/OpenFeature/Hook.cs

+15-6
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public abstract class Hook
3131
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
3232
/// <returns>Modified EvaluationContext that is used for the flag evaluation</returns>
3333
public virtual ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> context,
34-
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
34+
IReadOnlyDictionary<string, object>? hints = null,
35+
CancellationToken cancellationToken = default)
3536
{
3637
return new ValueTask<EvaluationContext>(EvaluationContext.Empty);
3738
}
@@ -44,8 +45,10 @@ public virtual ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> contex
4445
/// <param name="hints">Caller provided data</param>
4546
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
4647
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
47-
public virtual ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> details,
48-
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
48+
public virtual ValueTask AfterAsync<T>(HookContext<T> context,
49+
FlagEvaluationDetails<T> details,
50+
IReadOnlyDictionary<string, object>? hints = null,
51+
CancellationToken cancellationToken = default)
4952
{
5053
return new ValueTask();
5154
}
@@ -58,8 +61,10 @@ public virtual ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDet
5861
/// <param name="hints">Caller provided data</param>
5962
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
6063
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
61-
public virtual ValueTask ErrorAsync<T>(HookContext<T> context, Exception error,
62-
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
64+
public virtual ValueTask ErrorAsync<T>(HookContext<T> context,
65+
Exception error,
66+
IReadOnlyDictionary<string, object>? hints = null,
67+
CancellationToken cancellationToken = default)
6368
{
6469
return new ValueTask();
6570
}
@@ -68,10 +73,14 @@ public virtual ValueTask ErrorAsync<T>(HookContext<T> context, Exception error,
6873
/// Called unconditionally after flag evaluation.
6974
/// </summary>
7075
/// <param name="context">Provides context of innovation</param>
76+
/// <param name="evaluationDetails">Flag evaluation information</param>
7177
/// <param name="hints">Caller provided data</param>
7278
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
7379
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
74-
public virtual ValueTask FinallyAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
80+
public virtual ValueTask FinallyAsync<T>(HookContext<T> context,
81+
FlagEvaluationDetails<T> evaluationDetails,
82+
IReadOnlyDictionary<string, object>? hints = null,
83+
CancellationToken cancellationToken = default)
7584
{
7685
return new ValueTask();
7786
}

src/OpenFeature/OpenFeatureClient.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlagAsync<T>(
244244
evaluationContextBuilder.Build()
245245
);
246246

247-
FlagEvaluationDetails<T> evaluation;
247+
FlagEvaluationDetails<T>? evaluation = null;
248248
try
249249
{
250250
var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false);
@@ -297,7 +297,9 @@ await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, opti
297297
}
298298
finally
299299
{
300-
await this.TriggerFinallyHooksAsync(allHooksReversed, hookContext, options, cancellationToken).ConfigureAwait(false);
300+
evaluation ??= new FlagEvaluationDetails<T>(flagKey, defaultValue, ErrorType.General, Reason.Error, string.Empty,
301+
"Evaluation failed to return a result.");
302+
await this.TriggerFinallyHooksAsync(allHooksReversed, evaluation, hookContext, options, cancellationToken).ConfigureAwait(false);
301303
}
302304

303305
return evaluation;
@@ -351,14 +353,14 @@ private async Task TriggerErrorHooksAsync<T>(IReadOnlyList<Hook> hooks, HookCont
351353
}
352354
}
353355

354-
private async Task TriggerFinallyHooksAsync<T>(IReadOnlyList<Hook> hooks, HookContext<T> context,
355-
FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
356+
private async Task TriggerFinallyHooksAsync<T>(IReadOnlyList<Hook> hooks, FlagEvaluationDetails<T> evaluation,
357+
HookContext<T> context, FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
356358
{
357359
foreach (var hook in hooks)
358360
{
359361
try
360362
{
361-
await hook.FinallyAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false);
363+
await hook.FinallyAsync(context, evaluation, options?.HookHints, cancellationToken).ConfigureAwait(false);
362364
}
363365
catch (Exception e)
364366
{

test/OpenFeature.Tests/OpenFeatureClientTests.cs

+20
Original file line numberDiff line numberDiff line change
@@ -656,5 +656,25 @@ public async Task TheClient_MergesTheEvaluationContextInTheCorrectOrder(string k
656656

657657
Assert.Equal(expectedResult, actualEvaluationContext.GetValue(key).AsString);
658658
}
659+
660+
[Fact]
661+
[Specification("4.3.8", "'evaluation details' passed to the 'finally' stage matches the evaluation details returned to the application author")]
662+
public async Task FinallyHook_IncludesEvaluationDetails()
663+
{
664+
// Arrange
665+
var provider = new TestProvider();
666+
var providerHook = Substitute.For<Hook>();
667+
provider.AddHook(providerHook);
668+
await Api.Instance.SetProviderAsync(provider);
669+
var client = Api.Instance.GetClient();
670+
671+
const string flagName = "flagName";
672+
673+
// Act
674+
var evaluationDetails = await client.GetBooleanDetailsAsync(flagName, true);
675+
676+
// Assert
677+
await providerHook.Received(1).FinallyAsync(Arg.Any<HookContext<bool>>(), evaluationDetails);
678+
}
659679
}
660680
}

0 commit comments

Comments
 (0)