From 859710a8453cb44eb8ee6914329ace88ce17719f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 20 Dec 2024 09:22:52 +0000
Subject: [PATCH 1/6] Adding evaluation details and formatting.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
src/OpenFeature/Hook.cs | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs
index aea5dc15..c1dbbe38 100644
--- a/src/OpenFeature/Hook.cs
+++ b/src/OpenFeature/Hook.cs
@@ -31,7 +31,8 @@ public abstract class Hook
/// Flag value type (bool|number|string|object)
/// Modified EvaluationContext that is used for the flag evaluation
public virtual ValueTask BeforeAsync(HookContext context,
- IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ IReadOnlyDictionary? hints = null,
+ CancellationToken cancellationToken = default)
{
return new ValueTask(EvaluationContext.Empty);
}
@@ -44,8 +45,10 @@ public virtual ValueTask BeforeAsync(HookContext contex
/// Caller provided data
/// The .
/// Flag value type (bool|number|string|object)
- public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details,
- IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ public virtual ValueTask AfterAsync(HookContext context,
+ FlagEvaluationDetails details,
+ IReadOnlyDictionary? hints = null,
+ CancellationToken cancellationToken = default)
{
return new ValueTask();
}
@@ -58,8 +61,10 @@ public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDet
/// Caller provided data
/// The .
/// Flag value type (bool|number|string|object)
- public virtual ValueTask ErrorAsync(HookContext context, Exception error,
- IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ public virtual ValueTask ErrorAsync(HookContext context,
+ Exception error,
+ IReadOnlyDictionary? hints = null,
+ CancellationToken cancellationToken = default)
{
return new ValueTask();
}
@@ -68,10 +73,14 @@ public virtual ValueTask ErrorAsync(HookContext context, Exception error,
/// Called unconditionally after flag evaluation.
///
/// Provides context of innovation
+ /// Flag evaluation information
/// Caller provided data
/// The .
/// Flag value type (bool|number|string|object)
- public virtual ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ public virtual ValueTask FinallyAsync(HookContext context,
+ FlagEvaluationDetails evaluationDetails,
+ IReadOnlyDictionary? hints = null,
+ CancellationToken cancellationToken = default)
{
return new ValueTask();
}
From bca8a71df093981439232242d00c0db7a442a0ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 20 Dec 2024 13:21:50 +0000
Subject: [PATCH 2/6] Adding evaluation to the hook call.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
src/OpenFeature/OpenFeatureClient.cs | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs
index e774c6b5..0f1f51d3 100644
--- a/src/OpenFeature/OpenFeatureClient.cs
+++ b/src/OpenFeature/OpenFeatureClient.cs
@@ -244,7 +244,7 @@ private async Task> EvaluateFlagAsync(
evaluationContextBuilder.Build()
);
- FlagEvaluationDetails evaluation;
+ FlagEvaluationDetails? evaluation = null;
try
{
var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false);
@@ -252,10 +252,12 @@ private async Task> EvaluateFlagAsync(
// short circuit evaluation entirely if provider is in a bad state
if (provider.Status == ProviderStatus.NotReady)
{
+ evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ErrorType.ProviderNotReady, Reason.Error, string.Empty, "Provider has not yet completed initialization.");
throw new ProviderNotReadyException("Provider has not yet completed initialization.");
}
else if (provider.Status == ProviderStatus.Fatal)
{
+ evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ErrorType.ProviderFatal, Reason.Error, string.Empty, string.Empty);
throw new ProviderFatalException("Provider is in an irrecoverable error state.");
}
@@ -297,7 +299,9 @@ await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, opti
}
finally
{
- await this.TriggerFinallyHooksAsync(allHooksReversed, hookContext, options, cancellationToken).ConfigureAwait(false);
+ evaluation ??= new FlagEvaluationDetails(flagKey, defaultValue, ErrorType.General, Reason.Error, string.Empty,
+ "Evaluation failed to return a result.");
+ await this.TriggerFinallyHooksAsync(allHooksReversed, evaluation, hookContext, options, cancellationToken).ConfigureAwait(false);
}
return evaluation;
@@ -351,14 +355,14 @@ private async Task TriggerErrorHooksAsync(IReadOnlyList hooks, HookCont
}
}
- private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, HookContext context,
- FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
+ private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, FlagEvaluationDetails evaluation,
+ HookContext context, FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
{
foreach (var hook in hooks)
{
try
{
- await hook.FinallyAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false);
+ await hook.FinallyAsync(context, evaluation, options?.HookHints, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
From 7b9ea132336b4a70003e803ee087db19a6e84a3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 20 Dec 2024 13:29:59 +0000
Subject: [PATCH 3/6] Fixing unit tests.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../OpenFeature.Tests/OpenFeatureHookTests.cs | 70 ++++++++++---------
test/OpenFeature.Tests/TestImplementations.cs | 1 +
2 files changed, 37 insertions(+), 34 deletions(-)
diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs
index cc8b08a1..48de5ee5 100644
--- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs
+++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs
@@ -45,10 +45,10 @@ public async Task Hooks_Should_Be_Called_In_Order()
invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
- providerHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
- invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
- clientHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
- apiHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
+ providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
+ invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
+ clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
+ apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask());
var testProvider = new TestProvider();
testProvider.AddHook(providerHook);
@@ -70,10 +70,10 @@ await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empt
invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
- providerHook.FinallyAsync(Arg.Any>(), Arg.Any>());
- invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>());
- clientHook.FinallyAsync(Arg.Any>(), Arg.Any>());
- apiHook.FinallyAsync(Arg.Any>(), Arg.Any>());
+ providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
+ invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
+ clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
+ apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
});
_ = apiHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>());
@@ -84,10 +84,10 @@ await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empt
_ = invocationHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
_ = clientHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
_ = apiHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
- _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>());
- _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>());
- _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>());
- _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>());
+ _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
+ _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
+ _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
+ _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
}
[Fact]
@@ -239,10 +239,12 @@ public async Task Hook_Should_Return_No_Errors()
};
var hookContext = new HookContext("test", false, FlagValueType.Boolean,
new ClientMetadata(null, null), new Metadata(null), EvaluationContext.Empty);
+ var evaluationDetails =
+ new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing");
await hook.BeforeAsync(hookContext, hookHints);
- await hook.AfterAsync(hookContext, new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"), hookHints);
- await hook.FinallyAsync(hookContext, hookHints);
+ await hook.AfterAsync(hookContext, evaluationDetails, hookHints);
+ await hook.FinallyAsync(hookContext, evaluationDetails, hookHints);
await hook.ErrorAsync(hookContext, new Exception(), hookHints);
hookContext.ClientMetadata.Name.Should().BeNull();
@@ -269,7 +271,7 @@ public async Task Hook_Should_Execute_In_Correct_Order()
hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty);
featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false));
_ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
- _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>());
+ _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
await Api.Instance.SetProviderAsync(featureProvider);
var client = Api.Instance.GetClient();
@@ -282,12 +284,12 @@ public async Task Hook_Should_Execute_In_Correct_Order()
hook.BeforeAsync(Arg.Any>(), Arg.Any>());
featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any());
hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
- hook.FinallyAsync(Arg.Any>(), Arg.Any>());
+ hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
});
_ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>());
_ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
- _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>());
+ _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
_ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any());
}
@@ -331,8 +333,8 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination()
featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false));
hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask());
hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask());
- hook2.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask());
- hook1.FinallyAsync(Arg.Any>(), null).Throws(new Exception());
+ hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask());
+ hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Throws(new Exception());
await Api.Instance.SetProviderAsync(featureProvider);
var client = Api.Instance.GetClient();
@@ -348,8 +350,8 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination()
featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any());
hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null);
hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null);
- hook2.FinallyAsync(Arg.Any>(), null);
- hook1.FinallyAsync(Arg.Any>(), null);
+ hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null);
+ hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null);
});
_ = hook1.Received(1).BeforeAsync(Arg.Any>(), null);
@@ -357,8 +359,8 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination()
_ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any());
_ = hook2.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null);
_ = hook1.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null);
- _ = hook2.Received(1).FinallyAsync(Arg.Any>(), null);
- _ = hook1.Received(1).FinallyAsync(Arg.Any>(), null);
+ _ = hook2.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null);
+ _ = hook1.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null);
}
[Fact]
@@ -458,7 +460,7 @@ public async Task Hook_Hints_May_Be_Optional()
hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>())
.Returns(new ValueTask());
- hook.FinallyAsync(Arg.Any>(), Arg.Any>())
+ hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>())
.Returns(new ValueTask());
await Api.Instance.SetProviderAsync(featureProvider);
@@ -471,7 +473,7 @@ public async Task Hook_Hints_May_Be_Optional()
hook.Received().BeforeAsync(Arg.Any>(), Arg.Any>());
featureProvider.Received().ResolveBooleanValueAsync("test", false, Arg.Any());
hook.Received().AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
- hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>());
+ hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>());
});
}
@@ -489,7 +491,7 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value()
// Sequence
hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(exceptionToThrow);
hook.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask());
- hook.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask());
+ hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask());
var client = Api.Instance.GetClient();
client.AddHooks(hook);
@@ -500,13 +502,13 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value()
{
hook.BeforeAsync(Arg.Any>(), Arg.Any>());
hook.ErrorAsync(Arg.Any>(), Arg.Any