Skip to content

Commit 9f3f879

Browse files
committed
feat: update ctx merge order, add client ctx
Signed-off-by: Todd Baert <[email protected]>
1 parent 3ee2673 commit 9f3f879

File tree

6 files changed

+157
-4
lines changed

6 files changed

+157
-4
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,10 @@ ASALocalRun/
341341
/.sonarqube
342342

343343
/src/LastMajorVersionBinaries
344+
345+
# vscode
346+
.vscode/*
347+
!.vscode/settings.json
348+
!.vscode/tasks.json
349+
!.vscode/launch.json
350+
!.vscode/extensions.json

.vscode/launch.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
// Use IntelliSense to find out which attributes exist for C# debugging
6+
// Use hover for the description of the existing attributes
7+
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
8+
"name": ".NET Core Launch (console)",
9+
"type": "coreclr",
10+
"request": "launch",
11+
"preLaunchTask": "build",
12+
// If you have changed target frameworks, make sure to update the program path.
13+
"program": "${workspaceFolder}/test/OpenFeature.Tests/bin/Debug/net6.0/OpenFeature.Tests.dll",
14+
"args": [],
15+
"cwd": "${workspaceFolder}/test/OpenFeature.Tests",
16+
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17+
"console": "internalConsole",
18+
"stopAtEntry": false
19+
},
20+
{
21+
"name": ".NET Core Attach",
22+
"type": "coreclr",
23+
"request": "attach"
24+
}
25+
]
26+
}

.vscode/tasks.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "build",
6+
"command": "dotnet",
7+
"type": "process",
8+
"args": [
9+
"build",
10+
"${workspaceFolder}/test/OpenFeature.Tests/OpenFeature.Tests.csproj",
11+
"/property:GenerateFullPaths=true",
12+
"/consoleloggerparameters:NoSummary"
13+
],
14+
"problemMatcher": "$msCompile"
15+
},
16+
{
17+
"label": "publish",
18+
"command": "dotnet",
19+
"type": "process",
20+
"args": [
21+
"publish",
22+
"${workspaceFolder}/test/OpenFeature.Tests/OpenFeature.Tests.csproj",
23+
"/property:GenerateFullPaths=true",
24+
"/consoleloggerparameters:NoSummary"
25+
],
26+
"problemMatcher": "$msCompile"
27+
},
28+
{
29+
"label": "watch",
30+
"command": "dotnet",
31+
"type": "process",
32+
"args": [
33+
"watch",
34+
"run",
35+
"--project",
36+
"${workspaceFolder}/test/OpenFeature.Tests/OpenFeature.Tests.csproj"
37+
],
38+
"problemMatcher": "$msCompile"
39+
}
40+
]
41+
}

src/OpenFeature.SDK/OpenFeature.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ private OpenFeature() { }
5050
/// <param name="name">Name of client</param>
5151
/// <param name="version">Version of client</param>
5252
/// <param name="logger">Logger instance used by client</param>
53+
/// <param name="context">Context given to this client</param>
5354
/// <returns><see cref="FeatureClient"/></returns>
54-
public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null) =>
55-
new FeatureClient(this._featureProvider, name, version, logger);
55+
public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null, EvaluationContext context = null) =>
56+
new FeatureClient(this._featureProvider, name, version, logger, context);
5657

5758
/// <summary>
5859
/// Appends list of hooks to global hooks list

src/OpenFeature.SDK/OpenFeatureClient.cs

+12-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ public sealed class FeatureClient : IFeatureClient
2020
private readonly FeatureProvider _featureProvider;
2121
private readonly List<Hook> _hooks = new List<Hook>();
2222
private readonly ILogger _logger;
23+
private readonly EvaluationContext _evaluationContext;
24+
25+
/// <summary>
26+
/// Gets the client <see cref="EvaluationContext"/>
27+
/// </summary>
28+
/// <returns></returns>
29+
public EvaluationContext GetContext() => this._evaluationContext;
2330

2431
/// <summary>
2532
/// Initializes a new instance of the <see cref="FeatureClient"/> class.
@@ -28,12 +35,14 @@ public sealed class FeatureClient : IFeatureClient
2835
/// <param name="name">Name of client <see cref="ClientMetadata"/></param>
2936
/// <param name="version">Version of client <see cref="ClientMetadata"/></param>
3037
/// <param name="logger">Logger used by client</param>
38+
/// <param name="context">Context given to this client</param>
3139
/// <exception cref="ArgumentNullException">Throws if any of the required parameters are null</exception>
32-
public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null)
40+
public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null, EvaluationContext context = null)
3341
{
3442
this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider));
3543
this._metadata = new ClientMetadata(name, version);
3644
this._logger = logger ?? new Logger<OpenFeature>(new NullLoggerFactory());
45+
this._evaluationContext = context ?? new EvaluationContext();
3746
}
3847

3948
/// <summary>
@@ -202,7 +211,9 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlag<T>(
202211
context = new EvaluationContext();
203212
}
204213

214+
// merge api, client, and invocation context.
205215
var evaluationContext = OpenFeature.Instance.GetContext();
216+
evaluationContext.Merge(this.GetContext());
206217
evaluationContext.Merge(context);
207218

208219
var allHooks = new List<Hook>()

test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs

+68-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable()
164164
[Fact]
165165
[Specification("4.1.4", "The evaluation context MUST be mutable only within the `before` hook.")]
166166
[Specification("4.3.3", "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")]
167-
[Specification("4.3.4", "When `before` hooks have finished executing, any resulting `evaluation context` MUST be merged with the invocation `evaluation context` with the invocation `evaluation context` taking precedence in the case of any conflicts.")]
168167
public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook()
169168
{
170169
var evaluationContext = new EvaluationContext { ["test"] = "test" };
@@ -189,6 +188,74 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook()
189188
hook2.Verify(x => x.Before(It.Is<HookContext<bool>>(a => a.EvaluationContext.Get<string>("test") == "test"), It.IsAny<Dictionary<string, object>>()), Times.Once);
190189
}
191190

191+
[Fact]
192+
[Specification("4.3.4", "When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context in the following order: before-hook (highest precedence), invocation, client, api (lowest precedence).")]
193+
public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order()
194+
{
195+
var propGlobal = "4.3.4global";
196+
var propGlobalToOverwrite = "4.3.4globalToOverwrite";
197+
198+
var propClient = "4.3.4client";
199+
var propClientToOverwrite = "4.3.4clientToOverwrite";
200+
201+
var propInvocation = "4.3.4invocation";
202+
var propInvocationToOverwrite = "4.3.4invocationToOverwrite";
203+
204+
var propHook = "4.3.4hook";
205+
206+
// setup a cascade of overwriting properties
207+
OpenFeature.Instance.SetContext(new EvaluationContext {
208+
[propGlobal] = true,
209+
[propGlobalToOverwrite] = false
210+
});
211+
var clientContext = new EvaluationContext {
212+
[propClient] = true,
213+
[propGlobalToOverwrite] = true,
214+
[propClientToOverwrite] = false
215+
};
216+
var invocationContext = new EvaluationContext {
217+
[propInvocation] = true,
218+
[propClientToOverwrite] = true,
219+
[propInvocationToOverwrite] = false,
220+
};
221+
var hookContext = new EvaluationContext {
222+
[propHook] = true,
223+
[propInvocationToOverwrite] = true,
224+
};
225+
226+
var provider = new Mock<FeatureProvider>(MockBehavior.Strict);
227+
228+
provider.Setup(x => x.GetMetadata())
229+
.Returns(new Metadata(null));
230+
231+
provider.Setup(x => x.GetProviderHooks())
232+
.Returns(Array.Empty<Hook>());
233+
234+
provider.Setup(x => x.ResolveBooleanValue(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<EvaluationContext>(), null))
235+
.ReturnsAsync(new ResolutionDetails<bool>("test", true));
236+
237+
OpenFeature.Instance.SetProvider(provider.Object);
238+
239+
var hook = new Mock<Hook>(MockBehavior.Strict);
240+
hook.Setup(x => x.Before(It.IsAny<HookContext<It.IsAnyType>>(), It.IsAny<Dictionary<string, object>>()))
241+
.ReturnsAsync(hookContext);
242+
243+
244+
var client = OpenFeature.Instance.GetClient("test", "1.0.0", null, clientContext);
245+
await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(new[] { hook.Object }, new Dictionary<string, object>()));
246+
247+
// after proper merging, all properties should equal true
248+
provider.Verify(x => x.ResolveBooleanValue(It.IsAny<string>(), It.IsAny<bool>(), It.Is<EvaluationContext>(y =>
249+
y.Get<bool>(propGlobal)
250+
&& y.Get<bool>(propClient)
251+
&& y.Get<bool>(propGlobalToOverwrite)
252+
&& y.Get<bool>(propInvocation)
253+
&& y.Get<bool>(propClientToOverwrite)
254+
&& y.Get<bool>(propHook)
255+
&& y.Get<bool>(propInvocationToOverwrite)
256+
), It.IsAny<FlagEvaluationOptions>()), Times.Once);
257+
}
258+
192259
[Fact]
193260
[Specification("4.2.1", "`hook hints` MUST be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`..")]
194261
[Specification("4.2.2.1", "Condition: `Hook hints` MUST be immutable.")]

0 commit comments

Comments
 (0)