Skip to content

Commit 1264589

Browse files
committed
feat: Add support for hook data.
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
1 parent 9185b76 commit 1264589

File tree

6 files changed

+390
-114
lines changed

6 files changed

+390
-114
lines changed

src/OpenFeature/HookData.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using OpenFeature.Model;
5+
6+
namespace OpenFeature
7+
{
8+
/// <summary>
9+
/// A key-value collection of strings to objects used for passing data between hook stages.
10+
/// <para>
11+
/// This collection is scoped to a single evaluation for a single hook. Each hook stage for the evaluation
12+
/// will share the same <see cref="HookData"/>.
13+
/// </para>
14+
/// <para>
15+
/// This collection is intended for use only during the execution of individual hook stages, a reference
16+
/// to the collection should not be retained.
17+
/// </para>
18+
/// <para>
19+
/// This collection is not thread-safe.
20+
/// </para>
21+
/// </summary>
22+
/// <seealso href="https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md#46-hook-data"/>
23+
public sealed class HookData
24+
{
25+
private readonly Dictionary<string, object> _data = [];
26+
27+
/// <summary>
28+
/// Set the key to the given value.
29+
/// </summary>
30+
/// <param name="key">The key for the value</param>
31+
/// <param name="value">The value to set</param>
32+
public void Set(string key, object value)
33+
{
34+
this._data[key] = value;
35+
}
36+
37+
/// <summary>
38+
/// Gets the value at the specified key as an object.
39+
/// <remarks>
40+
/// For <see cref="Value"/> types use <see cref="Get"/> instead.
41+
/// </remarks>
42+
/// </summary>
43+
/// <param name="key">The key of the value to be retrieved</param>
44+
/// <returns>The object associated with the key</returns>
45+
/// <exception cref="KeyNotFoundException">
46+
/// Thrown when the context does not contain the specified key
47+
/// </exception>
48+
/// <exception cref="ArgumentNullException">
49+
/// Thrown when the key is <see langword="null" />
50+
/// </exception>
51+
public object Get(string key)
52+
{
53+
return this._data[key];
54+
}
55+
56+
/// <summary>
57+
/// Return a count of all values.
58+
/// </summary>
59+
public int Count => this._data.Count;
60+
61+
/// <summary>
62+
/// Return an enumerator for all values.
63+
/// </summary>
64+
/// <returns>An enumerator for all values</returns>
65+
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
66+
{
67+
return this._data.GetEnumerator();
68+
}
69+
70+
/// <summary>
71+
/// Return a list containing all the keys in the hook data
72+
/// </summary>
73+
public IImmutableList<string> Keys => this._data.Keys.ToImmutableList();
74+
75+
/// <summary>
76+
/// Return an enumerable containing all the values of the hook data
77+
/// </summary>
78+
public IImmutableList<object> Values => this._data.Values.ToImmutableList();
79+
80+
/// <summary>
81+
/// Gets all values as a read only dictionary.
82+
/// <remarks>
83+
/// The dictionary references the original values and is not a thread-safe copy.
84+
/// </remarks>
85+
/// </summary>
86+
/// <returns>A <see cref="IDictionary{TKey,TValue}"/> representation of the hook data</returns>
87+
public IReadOnlyDictionary<string, object> AsDictionary()
88+
{
89+
return this._data;
90+
}
91+
}
92+
}

src/OpenFeature/HookRunner.cs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Logging.Abstractions;
8+
using OpenFeature.Model;
9+
10+
namespace OpenFeature;
11+
12+
internal partial class HookRunner<T>
13+
{
14+
private readonly ImmutableList<Hook> _hooks;
15+
16+
private readonly List<HookContext<T>> _hookContexts;
17+
18+
private EvaluationContext _evaluationContext;
19+
20+
private readonly ILogger _logger;
21+
22+
public HookRunner(ImmutableList<Hook>? hooks, EvaluationContext evaluationContext,
23+
SharedHookContext<T> sharedHookContext,
24+
ILogger? logger = null)
25+
{
26+
this._evaluationContext = evaluationContext;
27+
this._logger = logger ?? NullLogger<FeatureClient>.Instance;
28+
this._hooks = hooks ?? throw new ArgumentNullException(nameof(hooks));
29+
this._hookContexts = new List<HookContext<T>>(hooks.Count);
30+
for (var i = 0; i < hooks.Count; i++)
31+
{
32+
this._hookContexts.Add(sharedHookContext.ToHookContext(evaluationContext));
33+
}
34+
}
35+
36+
public async Task<EvaluationContext> TriggerBeforeHooksAsync(FlagEvaluationOptions? options,
37+
CancellationToken cancellationToken = default)
38+
{
39+
var evalContextBuilder = EvaluationContext.Builder();
40+
evalContextBuilder.Merge(this._evaluationContext);
41+
42+
for (var i = 0; i < this._hooks.Count; i++)
43+
{
44+
var hook = this._hooks[i];
45+
var hookContext = this._hookContexts[i];
46+
47+
var resp = await hook.BeforeAsync(hookContext, options?.HookHints, cancellationToken).ConfigureAwait(false);
48+
if (resp != null)
49+
{
50+
evalContextBuilder.Merge(resp);
51+
this._evaluationContext = evalContextBuilder.Build();
52+
for (var j = 0; j < this._hookContexts.Count; j++)
53+
{
54+
this._hookContexts[j] = this._hookContexts[j].WithNewEvaluationContext(this._evaluationContext);
55+
}
56+
}
57+
else
58+
{
59+
this.HookReturnedNull(hook.GetType().Name);
60+
}
61+
}
62+
63+
return this._evaluationContext;
64+
}
65+
66+
public async Task TriggerAfterHooksAsync(FlagEvaluationDetails<T> evaluationDetails,
67+
FlagEvaluationOptions? options,
68+
CancellationToken cancellationToken = default)
69+
{
70+
// After hooks run in reverse.
71+
for (var i = this._hooks.Count - 1; i >= 0; i--)
72+
{
73+
var hook = this._hooks[i];
74+
var hookContext = this._hookContexts[i];
75+
await hook.AfterAsync(hookContext, evaluationDetails, options?.HookHints, cancellationToken)
76+
.ConfigureAwait(false);
77+
}
78+
}
79+
80+
public async Task TriggerErrorHooksAsync(Exception exception,
81+
FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
82+
{
83+
// Error hooks run in reverse.
84+
for (var i = this._hooks.Count - 1; i >= 0; i--)
85+
{
86+
var hook = this._hooks[i];
87+
var hookContext = this._hookContexts[i];
88+
try
89+
{
90+
await hook.ErrorAsync(hookContext, exception, options?.HookHints, cancellationToken)
91+
.ConfigureAwait(false);
92+
}
93+
catch (Exception e)
94+
{
95+
this.ErrorHookError(hook.GetType().Name, e);
96+
}
97+
}
98+
}
99+
100+
public async Task TriggerFinallyHooksAsync(FlagEvaluationDetails<T> evaluation, FlagEvaluationOptions? options,
101+
CancellationToken cancellationToken = default)
102+
{
103+
// Finally hooks run in reverse
104+
for (var i = this._hooks.Count - 1; i >= 0; i--)
105+
{
106+
var hook = this._hooks[i];
107+
var hookContext = this._hookContexts[i];
108+
try
109+
{
110+
await hook.FinallyAsync(hookContext, evaluation, options?.HookHints, cancellationToken)
111+
.ConfigureAwait(false);
112+
}
113+
catch (Exception e)
114+
{
115+
this.FinallyHookError(hook.GetType().Name, e);
116+
}
117+
}
118+
}
119+
120+
[LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")]
121+
partial void HookReturnedNull(string hookName);
122+
123+
[LoggerMessage(103, LogLevel.Error, "Error while executing Error hook {HookName}")]
124+
partial void ErrorHookError(string hookName, Exception exception);
125+
126+
[LoggerMessage(104, LogLevel.Error, "Error while executing Finally hook {HookName}")]
127+
partial void FinallyHookError(string hookName, Exception exception);
128+
}

src/OpenFeature/Model/HookContext.cs

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ namespace OpenFeature.Model
1010
/// <seealso href="https://github.com/open-feature/spec/blob/v0.5.2/specification/sections/04-hooks.md#41-hook-context"/>
1111
public sealed class HookContext<T>
1212
{
13+
private readonly SharedHookContext<T> _shared;
14+
1315
/// <summary>
1416
/// Feature flag being evaluated
1517
/// </summary>
16-
public string FlagKey { get; }
18+
public string FlagKey => this._shared.FlagKey;
1719

1820
/// <summary>
1921
/// Default value if flag fails to be evaluated
2022
/// </summary>
21-
public T DefaultValue { get; }
23+
public T DefaultValue => this._shared.DefaultValue;
2224

2325
/// <summary>
2426
/// The value type of the flag
2527
/// </summary>
26-
public FlagValueType FlagValueType { get; }
28+
public FlagValueType FlagValueType => this._shared.FlagValueType;
2729

2830
/// <summary>
2931
/// User defined evaluation context used in the evaluation process
@@ -34,12 +36,17 @@ public sealed class HookContext<T>
3436
/// <summary>
3537
/// Client metadata
3638
/// </summary>
37-
public ClientMetadata ClientMetadata { get; }
39+
public ClientMetadata ClientMetadata => this._shared.ClientMetadata;
3840

3941
/// <summary>
4042
/// Provider metadata
4143
/// </summary>
42-
public Metadata ProviderMetadata { get; }
44+
public Metadata ProviderMetadata => this._shared.ProviderMetadata;
45+
46+
/// <summary>
47+
/// Hook data
48+
/// </summary>
49+
public HookData Data { get; }
4350

4451
/// <summary>
4552
/// Initialize a new instance of <see cref="HookContext{T}"/>
@@ -58,23 +65,27 @@ public HookContext(string? flagKey,
5865
Metadata? providerMetadata,
5966
EvaluationContext? evaluationContext)
6067
{
61-
this.FlagKey = flagKey ?? throw new ArgumentNullException(nameof(flagKey));
62-
this.DefaultValue = defaultValue;
63-
this.FlagValueType = flagValueType;
64-
this.ClientMetadata = clientMetadata ?? throw new ArgumentNullException(nameof(clientMetadata));
65-
this.ProviderMetadata = providerMetadata ?? throw new ArgumentNullException(nameof(providerMetadata));
68+
this._shared = new SharedHookContext<T>(
69+
flagKey, defaultValue, flagValueType, clientMetadata, providerMetadata);
70+
71+
this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
72+
this.Data = new HookData();
73+
}
74+
75+
internal HookContext(SharedHookContext<T> sharedHookContext, EvaluationContext? evaluationContext,
76+
HookData? hookData)
77+
{
78+
this._shared = sharedHookContext ?? throw new ArgumentNullException(nameof(sharedHookContext));
6679
this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
80+
this.Data = hookData ?? throw new ArgumentNullException(nameof(hookData));
6781
}
6882

6983
internal HookContext<T> WithNewEvaluationContext(EvaluationContext context)
7084
{
7185
return new HookContext<T>(
72-
this.FlagKey,
73-
this.DefaultValue,
74-
this.FlagValueType,
75-
this.ClientMetadata,
76-
this.ProviderMetadata,
77-
context
86+
this._shared,
87+
context,
88+
this.Data
7889
);
7990
}
8091
}

0 commit comments

Comments
 (0)