From c692ec2a26eb4007ff428e54eaa67ea22fd20728 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 15 Apr 2025 07:05:07 +0100
Subject: [PATCH 001/126] chore(deps): update codecov/codecov-action action to
v5.4.2 (#432)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
|
[codecov/codecov-action](https://github.com/codecov/codecov-action)
| action | patch | `v5.4.0` -> `v5.4.2` |
---
### Release Notes
codecov/codecov-action (codecov/codecov-action)
###
[`v5.4.2`](https://github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v542)
[Compare
Source](https://github.com/codecov/codecov-action/compare/v5.4.1...v5.4.2)
##### What's Changed
**Full Changelog**:
https://github.com/codecov/codecov-action/compare/v5.4.1..v5.4.2
###
[`v5.4.1`](https://github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v541)
[Compare
Source](https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.1)
##### What's Changed
- fix: use the github core methods by
[@thomasrockhu-codecov](https://github.com/thomasrockhu-codecov)
in
[https://github.com/codecov/codecov-action/pull/1807](https://github.com/codecov/codecov-action/pull/1807)
- build(deps): bump github/codeql-action from 3.28.12 to 3.28.13 by
[@app/dependabot](https://github.com/app/dependabot) in
[https://github.com/codecov/codecov-action/pull/1803](https://github.com/codecov/codecov-action/pull/1803)
- build(deps): bump github/codeql-action from 3.28.11 to 3.28.12 by
[@app/dependabot](https://github.com/app/dependabot) in
[https://github.com/codecov/codecov-action/pull/1797](https://github.com/codecov/codecov-action/pull/1797)
- build(deps): bump actions/upload-artifact from 4.6.1 to 4.6.2 by
[@app/dependabot](https://github.com/app/dependabot) in
[https://github.com/codecov/codecov-action/pull/1798](https://github.com/codecov/codecov-action/pull/1798)
- chore(release): wrapper -0.2.1 by
[@app/codecov-releaser-app](https://github.com/app/codecov-releaser-app)
in
[https://github.com/codecov/codecov-action/pull/1788](https://github.com/codecov/codecov-action/pull/1788)
- build(deps): bump github/codeql-action from 3.28.10 to 3.28.11 by
[@app/dependabot](https://github.com/app/dependabot) in
[https://github.com/codecov/codecov-action/pull/1786](https://github.com/codecov/codecov-action/pull/1786)
**Full Changelog**:
https://github.com/codecov/codecov-action/compare/v5.4.0..v5.4.1
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/open-feature/dotnet-sdk).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/code-coverage.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index 383136286..be0a5412b 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -34,7 +34,7 @@ jobs:
- name: Run Test
run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
- - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
+ - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
with:
name: Code Coverage for ${{ matrix.os }}
fail_ci_if_error: true
From 2de3a971600242d795431caf54fd956df9cbcb83 Mon Sep 17 00:00:00 2001
From: Kyle <38759683+kylejuliandev@users.noreply.github.com>
Date: Wed, 16 Apr 2025 09:18:07 +0100
Subject: [PATCH 002/126] test: Add E2E Steps for contextMerging.feature tests
(#422)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## This PR
- Add new Reqnroll steps for contextMerging.feature ahead of merging
#395
### Related Issues
Fixes #399
### Notes
I was following the [requirement
3.2.3](https://openfeature.dev/specification/sections/evaluation-context#requirement-323)
I'm assuming with these additions that "Before Hook" is the same as
"Invocation" in the dotnet-sdk. If this is not the case then please let
me know!
In order to fetch the merged context I use a test hook with a function
that can set the EvaluationContext on the state. There are probably
better abstractions to use
I haven't updated the spec submodule in these changes either, although I
can add that if needed
Let me know if you have any concerns or feedback 🏗️
### Follow-up Tasks
### How to test
---------
Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com>
---
spec | 2 +-
.../Steps/BaseStepDefinitions.cs | 117 +++++++++++++++++-
...ContextMergingPrecedenceStepDefinitions.cs | 35 ++++++
.../Utils/ContextStoringProvider.cs | 46 +++++++
test/OpenFeature.E2ETests/Utils/State.cs | 3 +
5 files changed, 200 insertions(+), 3 deletions(-)
create mode 100644 test/OpenFeature.E2ETests/Steps/ContextMergingPrecedenceStepDefinitions.cs
create mode 100644 test/OpenFeature.E2ETests/Utils/ContextStoringProvider.cs
diff --git a/spec b/spec
index 0cd553d85..27e4461b4 160000
--- a/spec
+++ b/spec
@@ -1 +1 @@
-Subproject commit 0cd553d85f03b0b7ab983a988ce1720a3190bc88
+Subproject commit 27e4461b452429ec64bcb89af0301f7664cb702a
diff --git a/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs
index 1e8311aef..6b2bfebfc 100644
--- a/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs
+++ b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs
@@ -1,4 +1,6 @@
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using OpenFeature.E2ETests.Utils;
using OpenFeature.Model;
@@ -18,10 +20,10 @@ public BaseStepDefinitions(State state)
}
[Given(@"a stable provider")]
- public void GivenAStableProvider()
+ public async Task GivenAStableProvider()
{
var memProvider = new InMemoryProvider(E2EFlagConfig);
- Api.Instance.SetProviderAsync(memProvider).Wait();
+ await Api.Instance.SetProviderAsync(memProvider).ConfigureAwait(false);
this.State.Client = Api.Instance.GetClient("TestClient", "1.0.0");
}
@@ -57,6 +59,54 @@ public void GivenAString_FlagWithKeyAndADefaultValue(string key, string defaultT
this.State.Flag = flagState;
}
+ [Given("a stable provider with retrievable context is registered")]
+ public async Task GivenAStableProviderWithRetrievableContextIsRegistered()
+ {
+ this.State.ContextStoringProvider = new ContextStoringProvider();
+
+ await Api.Instance.SetProviderAsync(this.State.ContextStoringProvider).ConfigureAwait(false);
+
+ Api.Instance.SetTransactionContextPropagator(new AsyncLocalTransactionContextPropagator());
+
+ this.State.Client = Api.Instance.GetClient("TestClient", "1.0.0");
+ }
+
+ [Given(@"A context entry with key ""(.*)"" and value ""(.*)"" is added to the ""(.*)"" level")]
+ public void GivenAContextEntryWithKeyAndValueIsAddedToTheLevel(string key, string value, string level)
+ {
+ var context = EvaluationContext.Builder()
+ .Set(key, value)
+ .Build();
+
+ this.InitializeContext(level, context);
+ }
+
+ [Given("A table with levels of increasing precedence")]
+ public void GivenATableWithLevelsOfIncreasingPrecedence(DataTable dataTable)
+ {
+ var items = dataTable.Rows.ToList();
+
+ var levels = items.Select(r => r.Values.First());
+
+ this.State.ContextPrecedenceLevels = levels.ToArray();
+ }
+
+ [Given(@"Context entries for each level from API level down to the ""(.*)"" level, with key ""(.*)"" and value ""(.*)""")]
+ public void GivenContextEntriesForEachLevelFromAPILevelDownToTheLevelWithKeyAndValue(string currentLevel, string key, string value)
+ {
+ if (this.State.ContextPrecedenceLevels == null)
+ this.State.ContextPrecedenceLevels = new string[0];
+
+ foreach (var level in this.State.ContextPrecedenceLevels)
+ {
+ var context = EvaluationContext.Builder()
+ .Set(key, value)
+ .Build();
+
+ this.InitializeContext(level, context);
+ }
+ }
+
[When(@"the flag was evaluated with details")]
public async Task WhenTheFlagWasEvaluatedWithDetails()
{
@@ -82,6 +132,54 @@ public async Task WhenTheFlagWasEvaluatedWithDetails()
break;
}
}
+ private void InitializeContext(string level, EvaluationContext context)
+ {
+ switch (level)
+ {
+ case "API":
+ {
+ Api.Instance.SetContext(context);
+ break;
+ }
+ case "Transaction":
+ {
+ Api.Instance.SetTransactionContext(context);
+ break;
+ }
+ case "Client":
+ {
+ if (this.State.Client != null)
+ {
+ this.State.Client.SetContext(context);
+ }
+ else
+ {
+ throw new PendingStepException("You must initialise a FeatureClient before adding some EvaluationContext");
+ }
+ break;
+ }
+ case "Invocation":
+ {
+ this.State.InvocationEvaluationContext = context;
+ break;
+ }
+ case "Before Hooks": // Assumed before hooks is the same as Invocation
+ {
+ if (this.State.Client != null)
+ {
+ this.State.Client.AddHooks(new BeforeHook(context));
+ }
+ else
+ {
+ throw new PendingStepException("You must initialise a FeatureClient before adding some EvaluationContext");
+ }
+
+ break;
+ }
+ default:
+ throw new PendingStepException("Context level not defined");
+ }
+ }
private static readonly IDictionary E2EFlagConfig = new Dictionary
{
@@ -159,4 +257,19 @@ public async Task WhenTheFlagWasEvaluatedWithDetails()
)
}
};
+
+ public class BeforeHook : Hook
+ {
+ private readonly EvaluationContext context;
+
+ public BeforeHook(EvaluationContext context)
+ {
+ this.context = context;
+ }
+
+ public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ return new ValueTask(this.context);
+ }
+ }
}
diff --git a/test/OpenFeature.E2ETests/Steps/ContextMergingPrecedenceStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/ContextMergingPrecedenceStepDefinitions.cs
new file mode 100644
index 000000000..c9f454ac9
--- /dev/null
+++ b/test/OpenFeature.E2ETests/Steps/ContextMergingPrecedenceStepDefinitions.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using OpenFeature.E2ETests.Utils;
+using Reqnroll;
+using Xunit;
+
+namespace OpenFeature.E2ETests.Steps;
+
+[Binding]
+[Scope(Feature = "Context merging precedence")]
+public class ContextMergingPrecedenceStepDefinitions : BaseStepDefinitions
+{
+ public ContextMergingPrecedenceStepDefinitions(State state) : base(state)
+ {
+ }
+
+ [When("Some flag was evaluated")]
+ public async Task WhenSomeFlagWasEvaluated()
+ {
+ this.State.Flag = new FlagState("boolean-flag", "true", FlagType.Boolean);
+ this.State.FlagResult = await this.State.Client!.GetBooleanValueAsync("boolean-flag", true, this.State.InvocationEvaluationContext).ConfigureAwait(false);
+ }
+
+ [Then(@"The merged context contains an entry with key ""(.*)"" and value ""(.*)""")]
+ public void ThenTheMergedContextContainsAnEntryWithKeyAndValue(string key, string value)
+ {
+ var provider = this.State.ContextStoringProvider;
+
+ var mergedContext = provider!.EvaluationContext!;
+
+ Assert.NotNull(mergedContext);
+
+ var actualValue = mergedContext.GetValue(key);
+ Assert.Contains(value, actualValue.AsString);
+ }
+}
diff --git a/test/OpenFeature.E2ETests/Utils/ContextStoringProvider.cs b/test/OpenFeature.E2ETests/Utils/ContextStoringProvider.cs
new file mode 100644
index 000000000..40141e791
--- /dev/null
+++ b/test/OpenFeature.E2ETests/Utils/ContextStoringProvider.cs
@@ -0,0 +1,46 @@
+using System.Threading;
+using System.Threading.Tasks;
+using OpenFeature.Model;
+
+namespace OpenFeature.E2ETests.Utils;
+
+public class ContextStoringProvider : FeatureProvider
+{
+ private EvaluationContext? evaluationContext;
+ public EvaluationContext? EvaluationContext { get => this.evaluationContext; }
+
+ public override Metadata? GetMetadata()
+ {
+ return new Metadata("ContextStoringProvider");
+ }
+
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ this.evaluationContext = context;
+ return Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ this.evaluationContext = context;
+ return Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ this.evaluationContext = context;
+ return Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ this.evaluationContext = context;
+ return Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+ }
+
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ this.evaluationContext = context;
+ return Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+ }
+}
diff --git a/test/OpenFeature.E2ETests/Utils/State.cs b/test/OpenFeature.E2ETests/Utils/State.cs
index b3380132f..13a4e5a39 100644
--- a/test/OpenFeature.E2ETests/Utils/State.cs
+++ b/test/OpenFeature.E2ETests/Utils/State.cs
@@ -10,4 +10,7 @@ public class State
public TestHook? TestHook;
public object? FlagResult;
public EvaluationContext? EvaluationContext;
+ public ContextStoringProvider? ContextStoringProvider;
+ public EvaluationContext? InvocationEvaluationContext;
+ public string[]? ContextPrecedenceLevels;
}
From 5608dfbd441b99531add8e89ad842ea9d613f707 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 16 Apr 2025 09:38:34 +0100
Subject: [PATCH 003/126] chore(deps): update spec digest to 18cde17 (#395)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| spec | digest | `27e4461` -> `18cde17` |
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/open-feature/dotnet-sdk).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
spec | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spec b/spec
index 27e4461b4..18cde1708 160000
--- a/spec
+++ b/spec
@@ -1 +1 @@
-Subproject commit 27e4461b452429ec64bcb89af0301f7664cb702a
+Subproject commit 18cde1708e08d4f380ba30b4b1649e06683edfd2
From 73207d037c68f91dc400f7962f60d51717d52beb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Wed, 16 Apr 2025 16:30:22 +0100
Subject: [PATCH 004/126] ci: Add GH Actions as a scanned artifact for CodeQL
(#436)
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>
---
.github/workflows/codeql-analysis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 2e3a1e149..57ca46418 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -32,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- language: [ 'csharp' ]
+ language: [ 'csharp', 'actions' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
From 568722a4ab1f863d8509dc4a172ac9c29f267825 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Thu, 17 Apr 2025 16:38:06 +0100
Subject: [PATCH 005/126] chore(workflows): Add permissions for contents and
pull-requests (#439)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## This PR
This pull request includes updates to several GitHub Actions workflows
to add permissions for reading contents and writing pull requests.
Additionally, there is a minor change to the `lint-pr.yml` workflow file
to standardize the quotation marks used in the `name` field.
Workflow permissions updates:
*
[`.github/workflows/ci.yml`](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fR15-R17):
Added permissions for reading contents and writing pull requests to the
`build` job.
*
[`.github/workflows/code-coverage.yml`](diffhunk://#diff-49708f979e226a1e7bd7a68d71b2e91aae8114dd3e9254d9830cd3b4d62d4303R15-R17):
Added permissions for reading contents and writing pull requests to the
`build-test-report` job.
*
[`.github/workflows/dco-merge-group.yml`](diffhunk://#diff-cbf8f01aa06b4aa3d0729c5bce44e4f919c801b55f19a781b15f62aa10e68e90R10-R12):
Added permissions for reading contents and writing pull requests to the
`DCO` job.
*
[`.github/workflows/dotnet-format.yml`](diffhunk://#diff-ca8c2611c79b991c0fbe04fec3c97c14dc83419f5efb1e8a7a96dd51e7df3e2aR12-R14):
Added permissions for reading contents and writing pull requests to the
`check-format` job.
*
[`.github/workflows/e2e.yml`](diffhunk://#diff-3e103440521ada06efd263ae09b259e5507e4b8f7408308dc227621ad9efa31eR16-R18):
Added permissions for reading contents and writing pull requests to the
`e2e-tests` job.
*
[`.github/workflows/lint-pr.yml`](diffhunk://#diff-70c3a017bfdb629fd50281fe5f7ad22e29c0ddac36e7065e9dc6d4f0924104f4R14-R16):
Added permissions for reading contents and writing pull requests to the
`main` job.
Standardization:
*
[`.github/workflows/lint-pr.yml`](diffhunk://#diff-70c3a017bfdb629fd50281fe5f7ad22e29c0ddac36e7065e9dc6d4f0924104f4L1-R1):
Changed single quotes to double quotes in the `name` field.
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.github/workflows/ci.yml | 3 +++
.github/workflows/code-coverage.yml | 3 +++
.github/workflows/dco-merge-group.yml | 3 +++
.github/workflows/dotnet-format.yml | 3 +++
.github/workflows/e2e.yml | 3 +++
.github/workflows/lint-pr.yml | 5 ++++-
6 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a26e59c6..bb1c72273 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,6 +12,9 @@ on:
jobs:
build:
+ permissions:
+ contents: read
+ pull-requests: write
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index be0a5412b..a33413d84 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -12,6 +12,9 @@ on:
jobs:
build-test-report:
+ permissions:
+ contents: read
+ pull-requests: write
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
diff --git a/.github/workflows/dco-merge-group.yml b/.github/workflows/dco-merge-group.yml
index 0241f80a8..018589ead 100644
--- a/.github/workflows/dco-merge-group.yml
+++ b/.github/workflows/dco-merge-group.yml
@@ -7,6 +7,9 @@ on:
jobs:
DCO:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
if: ${{ github.actor != 'renovate[bot]' }}
steps:
- run: echo "dummy DCO workflow (it won't run any check actually) to trigger by merge_group in order to enable merge queue"
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml
index 63259de07..16799cf11 100644
--- a/.github/workflows/dotnet-format.yml
+++ b/.github/workflows/dotnet-format.yml
@@ -9,6 +9,9 @@ on:
jobs:
check-format:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- name: Check out code
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index ce4bb634e..ae0ca8391 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -13,6 +13,9 @@ on:
jobs:
e2e-tests:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml
index 5dbb56887..f23079276 100644
--- a/.github/workflows/lint-pr.yml
+++ b/.github/workflows/lint-pr.yml
@@ -1,4 +1,4 @@
-name: 'Lint PR'
+name: "Lint PR"
on:
pull_request_target:
@@ -11,6 +11,9 @@ jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5
env:
From 456351216ce9113d84b56d0bce1dad39430a26cd Mon Sep 17 00:00:00 2001
From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Date: Fri, 18 Apr 2025 11:16:14 -0700
Subject: [PATCH 006/126] feat: Add support for hook data. (#387)
## This PR
Adds support for hook data.
https://github.com/open-feature/spec/pull/273
### Related Issues
### Notes
I realized that the 4.6.1 section of the spec wasn't consistent with the
expected usage. Basically it over-specifies the typing of the hook data
matching that of the evaluation context. That is one possible approach,
it would just mean a bit more work on the part of the hook implementers.
In the earlier example in the spec I put a `Span` in the hook data:
```
public Optional before(HookContext context, HookHints hints) {
SpanBuilder builder = tracer.spanBuilder('sample')
.setParent(Context.current().with(Span.current()));
Span span = builder.startSpan()
context.hookData.set("span", span);
}
public void after(HookContext context, FlagEvaluationDetails details, HookHints hints) {
// Only accessible by this hook for this specific evaluation.
Object value = context.hookData.get("span");
if (value instanceof Span) {
Span span = (Span) value;
span.end();
}
}
```
This is only possible if the hook data allows specification of any
`object` instead of being limited to the immutable types of a context.
For hooks hook data this is safe because only the hook mutating the data
will have access to that data. Additionally the execution of the hook
will be in sequence with the evaluation (likely in a single thread).
The alternative would be to store data in the hook, and use the hook
data to know when to remove it.
Something like this:
```
public Optional before(HookContext context, HookHints hints) {
SpanBuilder builder = tracer.spanBuilder('sample')
.setParent(Context.current().with(Span.current()));
Span span = builder.startSpan()
String storageId = Uuid();
this.tmpData.set(storageId, span);
context.hookData.set("span", storageId);
}
public void after(HookContext context, FlagEvaluationDetails details, HookHints hints) {
// Only accessible by this hook for this specific evaluation.
Object value = context.hookData.get("span");
if (value) {
String id = value.AsString();
Span span= this.tmpData.get(id);
span.end();
}
}
```
### Follow-up Tasks
### How to test
---------
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Co-authored-by: chrfwow
---
README.md | 27 ++-
src/OpenFeature/HookData.cs | 104 +++++++++++
src/OpenFeature/HookRunner.cs | 173 ++++++++++++++++++
src/OpenFeature/Model/HookContext.cs | 43 +++--
src/OpenFeature/OpenFeatureClient.cs | 127 ++++---------
src/OpenFeature/SharedHookContext.cs | 60 ++++++
test/OpenFeature.Tests/HookDataTests.cs | 158 ++++++++++++++++
.../OpenFeature.Tests/OpenFeatureHookTests.cs | 106 ++++++++++-
8 files changed, 683 insertions(+), 115 deletions(-)
create mode 100644 src/OpenFeature/HookData.cs
create mode 100644 src/OpenFeature/HookRunner.cs
create mode 100644 src/OpenFeature/SharedHookContext.cs
create mode 100644 test/OpenFeature.Tests/HookDataTests.cs
diff --git a/README.md b/README.md
index cf8101ab2..7a2261772 100644
--- a/README.md
+++ b/README.md
@@ -336,13 +336,13 @@ public class MyHook : Hook
}
public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details,
- IReadOnlyDictionary hints = null)
+ IReadOnlyDictionary? hints = null)
{
// code to run after successful flag evaluation
}
public ValueTask ErrorAsync(HookContext context, Exception error,
- IReadOnlyDictionary hints = null)
+ IReadOnlyDictionary? hints = null)
{
// code to run if there's an error during before hooks or during flag evaluation
}
@@ -354,6 +354,29 @@ public class MyHook : Hook
}
```
+Hooks support passing per-evaluation data between that stages using `hook data`. The below example hook uses `hook data` to measure the duration between the execution of the `before` and `after` stage.
+
+```csharp
+ class TimingHook : Hook
+ {
+ public ValueTask BeforeAsync(HookContext context,
+ IReadOnlyDictionary? hints = null)
+ {
+ context.Data.Set("beforeTime", DateTime.Now);
+ return ValueTask.FromResult(context.EvaluationContext);
+ }
+
+ public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details,
+ IReadOnlyDictionary? hints = null)
+ {
+ var beforeTime = context.Data.Get("beforeTime") as DateTime?;
+ var duration = DateTime.Now - beforeTime;
+ Console.WriteLine($"Duration: {duration}");
+ return ValueTask.CompletedTask;
+ }
+ }
+```
+
Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
### DependencyInjection
diff --git a/src/OpenFeature/HookData.cs b/src/OpenFeature/HookData.cs
new file mode 100644
index 000000000..5d56eb870
--- /dev/null
+++ b/src/OpenFeature/HookData.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using OpenFeature.Model;
+
+namespace OpenFeature
+{
+ ///
+ /// A key-value collection of strings to objects used for passing data between hook stages.
+ ///
+ /// This collection is scoped to a single evaluation for a single hook. Each hook stage for the evaluation
+ /// will share the same .
+ ///
+ ///
+ /// This collection is intended for use only during the execution of individual hook stages, a reference
+ /// to the collection should not be retained.
+ ///
+ ///
+ /// This collection is not thread-safe.
+ ///
+ ///
+ ///
+ public sealed class HookData
+ {
+ private readonly Dictionary _data = [];
+
+ ///
+ /// Set the key to the given value.
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This hook data instance
+ public HookData Set(string key, object value)
+ {
+ this._data[key] = value;
+ return this;
+ }
+
+ ///
+ /// Gets the value at the specified key as an object.
+ ///
+ /// For types use instead.
+ ///
+ ///
+ /// The key of the value to be retrieved
+ /// The object associated with the key
+ ///
+ /// Thrown when the context does not contain the specified key
+ ///
+ public object Get(string key)
+ {
+ return this._data[key];
+ }
+
+ ///
+ /// Return a count of all values.
+ ///
+ public int Count => this._data.Count;
+
+ ///
+ /// Return an enumerator for all values.
+ ///
+ /// An enumerator for all values
+ public IEnumerator> GetEnumerator()
+ {
+ return this._data.GetEnumerator();
+ }
+
+ ///
+ /// Return a list containing all the keys in the hook data
+ ///
+ public IImmutableList Keys => this._data.Keys.ToImmutableList();
+
+ ///
+ /// Return an enumerable containing all the values of the hook data
+ ///
+ public IImmutableList Values => this._data.Values.ToImmutableList();
+
+ ///
+ /// Gets all values as a read only dictionary.
+ ///
+ /// The dictionary references the original values and is not a thread-safe copy.
+ ///
+ ///
+ /// A representation of the hook data
+ public IReadOnlyDictionary AsDictionary()
+ {
+ return this._data;
+ }
+
+ ///
+ /// Gets or sets the value associated with the specified key.
+ ///
+ /// The key of the value to get or set
+ /// The value associated with the specified key
+ ///
+ /// Thrown when getting a value and the context does not contain the specified key
+ ///
+ public object this[string key]
+ {
+ get => this.Get(key);
+ set => this.Set(key, value);
+ }
+ }
+}
diff --git a/src/OpenFeature/HookRunner.cs b/src/OpenFeature/HookRunner.cs
new file mode 100644
index 000000000..8c1dbb510
--- /dev/null
+++ b/src/OpenFeature/HookRunner.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using OpenFeature.Model;
+
+namespace OpenFeature
+{
+ ///
+ /// This class manages the execution of hooks.
+ ///
+ /// type of the evaluation detail provided to the hooks
+ internal partial class HookRunner
+ {
+ private readonly ImmutableList _hooks;
+
+ private readonly List> _hookContexts;
+
+ private EvaluationContext _evaluationContext;
+
+ private readonly ILogger _logger;
+
+ ///
+ /// Construct a hook runner instance. Each instance should be used for the execution of a single evaluation.
+ ///
+ ///
+ /// The hooks for the evaluation, these should be in the correct order for the before evaluation stage
+ ///
+ ///
+ /// The initial evaluation context, this can be updated as the hooks execute
+ ///
+ ///
+ /// Contents of the initial hook context excluding the evaluation context and hook data
+ ///
+ /// Client logger instance
+ public HookRunner(ImmutableList hooks, EvaluationContext evaluationContext,
+ SharedHookContext sharedHookContext,
+ ILogger logger)
+ {
+ this._evaluationContext = evaluationContext;
+ this._logger = logger;
+ this._hooks = hooks;
+ this._hookContexts = new List>(hooks.Count);
+ for (var i = 0; i < hooks.Count; i++)
+ {
+ // Create hook instance specific hook context.
+ // Hook contexts are instance specific so that the mutable hook data is scoped to each hook.
+ this._hookContexts.Add(sharedHookContext.ToHookContext(evaluationContext));
+ }
+ }
+
+ ///
+ /// Execute before hooks.
+ ///
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ /// Context with any modifications from the before hooks
+ public async Task TriggerBeforeHooksAsync(IImmutableDictionary? hints,
+ CancellationToken cancellationToken = default)
+ {
+ var evalContextBuilder = EvaluationContext.Builder();
+ evalContextBuilder.Merge(this._evaluationContext);
+
+ for (var i = 0; i < this._hooks.Count; i++)
+ {
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
+
+ var resp = await hook.BeforeAsync(hookContext, hints, cancellationToken)
+ .ConfigureAwait(false);
+ if (resp != null)
+ {
+ evalContextBuilder.Merge(resp);
+ this._evaluationContext = evalContextBuilder.Build();
+ for (var j = 0; j < this._hookContexts.Count; j++)
+ {
+ this._hookContexts[j] = this._hookContexts[j].WithNewEvaluationContext(this._evaluationContext);
+ }
+ }
+ else
+ {
+ this.HookReturnedNull(hook.GetType().Name);
+ }
+ }
+
+ return this._evaluationContext;
+ }
+
+ ///
+ /// Execute the after hooks. These are executed in opposite order of the before hooks.
+ ///
+ /// The evaluation details which will be provided to the hook
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ public async Task TriggerAfterHooksAsync(FlagEvaluationDetails evaluationDetails,
+ IImmutableDictionary? hints,
+ CancellationToken cancellationToken = default)
+ {
+ // After hooks run in reverse.
+ for (var i = this._hooks.Count - 1; i >= 0; i--)
+ {
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
+ await hook.AfterAsync(hookContext, evaluationDetails, hints, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Execute the error hooks. These are executed in opposite order of the before hooks.
+ ///
+ /// Exception which triggered the error
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ public async Task TriggerErrorHooksAsync(Exception exception,
+ IImmutableDictionary? hints, CancellationToken cancellationToken = default)
+ {
+ // Error hooks run in reverse.
+ for (var i = this._hooks.Count - 1; i >= 0; i--)
+ {
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
+ try
+ {
+ await hook.ErrorAsync(hookContext, exception, hints, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ this.ErrorHookError(hook.GetType().Name, e);
+ }
+ }
+ }
+
+ ///
+ /// Execute the finally hooks. These are executed in opposite order of the before hooks.
+ ///
+ /// The evaluation details which will be provided to the hook
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ public async Task TriggerFinallyHooksAsync(FlagEvaluationDetails evaluationDetails,
+ IImmutableDictionary? hints,
+ CancellationToken cancellationToken = default)
+ {
+ // Finally hooks run in reverse
+ for (var i = this._hooks.Count - 1; i >= 0; i--)
+ {
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
+ try
+ {
+ await hook.FinallyAsync(hookContext, evaluationDetails, hints, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ this.FinallyHookError(hook.GetType().Name, e);
+ }
+ }
+ }
+
+ [LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")]
+ partial void HookReturnedNull(string hookName);
+
+ [LoggerMessage(103, LogLevel.Error, "Error while executing Error hook {HookName}")]
+ partial void ErrorHookError(string hookName, Exception exception);
+
+ [LoggerMessage(104, LogLevel.Error, "Error while executing Finally hook {HookName}")]
+ partial void FinallyHookError(string hookName, Exception exception);
+ }
+}
diff --git a/src/OpenFeature/Model/HookContext.cs b/src/OpenFeature/Model/HookContext.cs
index 69f58bde8..8d99a2836 100644
--- a/src/OpenFeature/Model/HookContext.cs
+++ b/src/OpenFeature/Model/HookContext.cs
@@ -10,20 +10,22 @@ namespace OpenFeature.Model
///
public sealed class HookContext
{
+ private readonly SharedHookContext _shared;
+
///
/// Feature flag being evaluated
///
- public string FlagKey { get; }
+ public string FlagKey => this._shared.FlagKey;
///
/// Default value if flag fails to be evaluated
///
- public T DefaultValue { get; }
+ public T DefaultValue => this._shared.DefaultValue;
///
/// The value type of the flag
///
- public FlagValueType FlagValueType { get; }
+ public FlagValueType FlagValueType => this._shared.FlagValueType;
///
/// User defined evaluation context used in the evaluation process
@@ -34,12 +36,17 @@ public sealed class HookContext
///
/// Client metadata
///
- public ClientMetadata ClientMetadata { get; }
+ public ClientMetadata ClientMetadata => this._shared.ClientMetadata;
///
/// Provider metadata
///
- public Metadata ProviderMetadata { get; }
+ public Metadata ProviderMetadata => this._shared.ProviderMetadata;
+
+ ///
+ /// Hook data
+ ///
+ public HookData Data { get; }
///
/// Initialize a new instance of
@@ -58,23 +65,27 @@ public HookContext(string? flagKey,
Metadata? providerMetadata,
EvaluationContext? evaluationContext)
{
- this.FlagKey = flagKey ?? throw new ArgumentNullException(nameof(flagKey));
- this.DefaultValue = defaultValue;
- this.FlagValueType = flagValueType;
- this.ClientMetadata = clientMetadata ?? throw new ArgumentNullException(nameof(clientMetadata));
- this.ProviderMetadata = providerMetadata ?? throw new ArgumentNullException(nameof(providerMetadata));
+ this._shared = new SharedHookContext(
+ flagKey, defaultValue, flagValueType, clientMetadata, providerMetadata);
+
+ this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
+ this.Data = new HookData();
+ }
+
+ internal HookContext(SharedHookContext? sharedHookContext, EvaluationContext? evaluationContext,
+ HookData? hookData)
+ {
+ this._shared = sharedHookContext ?? throw new ArgumentNullException(nameof(sharedHookContext));
this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
+ this.Data = hookData ?? throw new ArgumentNullException(nameof(hookData));
}
internal HookContext WithNewEvaluationContext(EvaluationContext context)
{
return new HookContext(
- this.FlagKey,
- this.DefaultValue,
- this.FlagValueType,
- this.ClientMetadata,
- this.ProviderMetadata,
- context
+ this._shared,
+ context,
+ this.Data
);
}
}
diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs
index 03420a2a4..4a00aa440 100644
--- a/src/OpenFeature/OpenFeatureClient.cs
+++ b/src/OpenFeature/OpenFeatureClient.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -222,32 +223,29 @@ private async Task> EvaluateFlagAsync(
evaluationContextBuilder.Merge(Api.Instance.GetTransactionContext()); // Transaction context
evaluationContextBuilder.Merge(context); // Invocation context
- var allHooks = new List()
+ var allHooks = ImmutableList.CreateBuilder()
.Concat(Api.Instance.GetHooks())
.Concat(this.GetHooks())
.Concat(options?.Hooks ?? Enumerable.Empty())
.Concat(provider.GetProviderHooks())
- .ToList()
- .AsReadOnly();
+ .ToImmutableList();
- var allHooksReversed = allHooks
- .AsEnumerable()
- .Reverse()
- .ToList()
- .AsReadOnly();
-
- var hookContext = new HookContext(
+ var sharedHookContext = new SharedHookContext(
flagKey,
defaultValue,
- flagValueType, this._metadata,
- provider.GetMetadata(),
- evaluationContextBuilder.Build()
+ flagValueType,
+ this._metadata,
+ provider.GetMetadata()
);
FlagEvaluationDetails? evaluation = null;
+ var hookRunner = new HookRunner(allHooks, evaluationContextBuilder.Build(), sharedHookContext,
+ this._logger);
+
try
{
- var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false);
+ var evaluationContextFromHooks = await hookRunner.TriggerBeforeHooksAsync(options?.HookHints, cancellationToken)
+ .ConfigureAwait(false);
// short circuit evaluation entirely if provider is in a bad state
if (provider.Status == ProviderStatus.NotReady)
@@ -260,23 +258,24 @@ private async Task> EvaluateFlagAsync(
}
evaluation =
- (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false))
+ (await resolveValueDelegate
+ .Invoke(flagKey, defaultValue, evaluationContextFromHooks, cancellationToken)
+ .ConfigureAwait(false))
.ToFlagEvaluationDetails();
if (evaluation.ErrorType == ErrorType.None)
{
- await this.TriggerAfterHooksAsync(
- allHooksReversed,
- hookContext,
+ await hookRunner.TriggerAfterHooksAsync(
evaluation,
- options,
+ options?.HookHints,
cancellationToken
).ConfigureAwait(false);
}
else
{
var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage);
- await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, options, cancellationToken)
+ this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception);
+ await hookRunner.TriggerErrorHooksAsync(exception, options?.HookHints, cancellationToken)
.ConfigureAwait(false);
}
}
@@ -285,88 +284,29 @@ await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, opti
this.FlagEvaluationErrorWithDescription(flagKey, ex.ErrorType.GetDescription(), ex);
evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error,
string.Empty, ex.Message);
- await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, ex, options, cancellationToken).ConfigureAwait(false);
+ await hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToken)
+ .ConfigureAwait(false);
}
catch (Exception ex)
{
var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General;
- evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty, ex.Message);
- await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, ex, options, cancellationToken).ConfigureAwait(false);
+ evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty,
+ ex.Message);
+ await hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToken)
+ .ConfigureAwait(false);
}
finally
{
- evaluation ??= new FlagEvaluationDetails(flagKey, defaultValue, ErrorType.General, Reason.Error, string.Empty,
+ 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);
+ await hookRunner.TriggerFinallyHooksAsync(evaluation, options?.HookHints, cancellationToken)
+ .ConfigureAwait(false);
}
return evaluation;
}
- private async Task> TriggerBeforeHooksAsync(IReadOnlyList hooks, HookContext context,
- FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
- {
- var evalContextBuilder = EvaluationContext.Builder();
- evalContextBuilder.Merge(context.EvaluationContext);
-
- foreach (var hook in hooks)
- {
- var resp = await hook.BeforeAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false);
- if (resp != null)
- {
- evalContextBuilder.Merge(resp);
- context = context.WithNewEvaluationContext(evalContextBuilder.Build());
- }
- else
- {
- this.HookReturnedNull(hook.GetType().Name);
- }
- }
-
- return context.WithNewEvaluationContext(evalContextBuilder.Build());
- }
-
- private async Task TriggerAfterHooksAsync(IReadOnlyList hooks, HookContext context,
- FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
- {
- foreach (var hook in hooks)
- {
- await hook.AfterAsync(context, evaluationDetails, options?.HookHints, cancellationToken).ConfigureAwait(false);
- }
- }
-
- private async Task TriggerErrorHooksAsync(IReadOnlyList hooks, HookContext context, Exception exception,
- FlagEvaluationOptions? options, CancellationToken cancellationToken = default)
- {
- foreach (var hook in hooks)
- {
- try
- {
- await hook.ErrorAsync(context, exception, options?.HookHints, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- this.ErrorHookError(hook.GetType().Name, e);
- }
- }
- }
-
- 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, evaluation, options?.HookHints, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- this.FinallyHookError(hook.GetType().Name, e);
- }
- }
- }
-
///
/// Use this method to track user interactions and the application state.
///
@@ -392,16 +332,13 @@ public void Track(string trackingEventName, EvaluationContext? evaluationContext
this._providerAccessor.Invoke().Track(trackingEventName, evaluationContextBuilder.Build(), trackingEventDetails);
}
+ [LoggerMessage(101, LogLevel.Error, "Error while evaluating flag {FlagKey}")]
+ partial void FlagEvaluationError(string flagKey, Exception exception);
+
[LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")]
partial void HookReturnedNull(string hookName);
[LoggerMessage(102, LogLevel.Error, "Error while evaluating flag {FlagKey}: {ErrorType}")]
partial void FlagEvaluationErrorWithDescription(string flagKey, string errorType, Exception exception);
-
- [LoggerMessage(103, LogLevel.Error, "Error while executing Error hook {HookName}")]
- partial void ErrorHookError(string hookName, Exception exception);
-
- [LoggerMessage(104, LogLevel.Error, "Error while executing Finally hook {HookName}")]
- partial void FinallyHookError(string hookName, Exception exception);
}
}
diff --git a/src/OpenFeature/SharedHookContext.cs b/src/OpenFeature/SharedHookContext.cs
new file mode 100644
index 000000000..3d6b787c6
--- /dev/null
+++ b/src/OpenFeature/SharedHookContext.cs
@@ -0,0 +1,60 @@
+using System;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+namespace OpenFeature
+{
+ ///
+ /// Component of the hook context which shared between all hook instances
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Flag value type
+ /// Client metadata
+ /// Provider metadata
+ /// Flag value type
+ internal class SharedHookContext(
+ string? flagKey,
+ T defaultValue,
+ FlagValueType flagValueType,
+ ClientMetadata? clientMetadata,
+ Metadata? providerMetadata)
+ {
+ ///
+ /// Feature flag being evaluated
+ ///
+ public string FlagKey { get; } = flagKey ?? throw new ArgumentNullException(nameof(flagKey));
+
+ ///
+ /// Default value if flag fails to be evaluated
+ ///
+ public T DefaultValue { get; } = defaultValue;
+
+ ///
+ /// The value type of the flag
+ ///
+ public FlagValueType FlagValueType { get; } = flagValueType;
+
+ ///
+ /// Client metadata
+ ///
+ public ClientMetadata ClientMetadata { get; } =
+ clientMetadata ?? throw new ArgumentNullException(nameof(clientMetadata));
+
+ ///
+ /// Provider metadata
+ ///
+ public Metadata ProviderMetadata { get; } =
+ providerMetadata ?? throw new ArgumentNullException(nameof(providerMetadata));
+
+ ///
+ /// Create a hook context from this shared context.
+ ///
+ /// Evaluation context
+ /// A hook context
+ public HookContext ToHookContext(EvaluationContext? evaluationContext)
+ {
+ return new HookContext(this, evaluationContext, new HookData());
+ }
+ }
+}
diff --git a/test/OpenFeature.Tests/HookDataTests.cs b/test/OpenFeature.Tests/HookDataTests.cs
new file mode 100644
index 000000000..96cbaf723
--- /dev/null
+++ b/test/OpenFeature.Tests/HookDataTests.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.Tests;
+
+public class HookDataTests
+{
+ private readonly HookData _commonHookData = new();
+
+ public HookDataTests()
+ {
+ this._commonHookData.Set("bool", true);
+ this._commonHookData.Set("string", "string");
+ this._commonHookData.Set("int", 1);
+ this._commonHookData.Set("double", 1.2);
+ this._commonHookData.Set("float", 1.2f);
+ }
+
+ [Fact]
+ public void HookData_Can_Set_And_Get_Data()
+ {
+ var hookData = new HookData();
+ hookData.Set("bool", true);
+ hookData.Set("string", "string");
+ hookData.Set("int", 1);
+ hookData.Set("double", 1.2);
+ hookData.Set("float", 1.2f);
+ var structure = Structure.Builder().Build();
+ hookData.Set("structure", structure);
+
+ Assert.True((bool)hookData.Get("bool"));
+ Assert.Equal("string", hookData.Get("string"));
+ Assert.Equal(1, hookData.Get("int"));
+ Assert.Equal(1.2, hookData.Get("double"));
+ Assert.Equal(1.2f, hookData.Get("float"));
+ Assert.Same(structure, hookData.Get("structure"));
+ }
+
+ [Fact]
+ public void HookData_Can_Chain_Set()
+ {
+ var structure = Structure.Builder().Build();
+
+ var hookData = new HookData();
+ hookData.Set("bool", true)
+ .Set("string", "string")
+ .Set("int", 1)
+ .Set("double", 1.2)
+ .Set("float", 1.2f)
+ .Set("structure", structure);
+
+ Assert.True((bool)hookData.Get("bool"));
+ Assert.Equal("string", hookData.Get("string"));
+ Assert.Equal(1, hookData.Get("int"));
+ Assert.Equal(1.2, hookData.Get("double"));
+ Assert.Equal(1.2f, hookData.Get("float"));
+ Assert.Same(structure, hookData.Get("structure"));
+ }
+
+ [Fact]
+ public void HookData_Can_Set_And_Get_Data_Using_Indexer()
+ {
+ var hookData = new HookData();
+ hookData["bool"] = true;
+ hookData["string"] = "string";
+ hookData["int"] = 1;
+ hookData["double"] = 1.2;
+ hookData["float"] = 1.2f;
+ var structure = Structure.Builder().Build();
+ hookData["structure"] = structure;
+
+ Assert.True((bool)hookData["bool"]);
+ Assert.Equal("string", hookData["string"]);
+ Assert.Equal(1, hookData["int"]);
+ Assert.Equal(1.2, hookData["double"]);
+ Assert.Equal(1.2f, hookData["float"]);
+ Assert.Same(structure, hookData["structure"]);
+ }
+
+ [Fact]
+ public void HookData_Can_Be_Enumerated()
+ {
+ var asList = new List>();
+ foreach (var kvp in this._commonHookData)
+ {
+ asList.Add(kvp);
+ }
+
+ asList.Sort((a, b) =>
+ string.Compare(a.Key, b.Key, StringComparison.Ordinal));
+
+ Assert.Equal([
+ new KeyValuePair("bool", true),
+ new KeyValuePair("double", 1.2),
+ new KeyValuePair("float", 1.2f),
+ new KeyValuePair("int", 1),
+ new KeyValuePair("string", "string")
+ ], asList);
+ }
+
+ [Fact]
+ public void HookData_Has_Count()
+ {
+ Assert.Equal(5, this._commonHookData.Count);
+ }
+
+ [Fact]
+ public void HookData_Has_Keys()
+ {
+ Assert.Equal(5, this._commonHookData.Keys.Count);
+ Assert.Contains("bool", this._commonHookData.Keys);
+ Assert.Contains("double", this._commonHookData.Keys);
+ Assert.Contains("float", this._commonHookData.Keys);
+ Assert.Contains("int", this._commonHookData.Keys);
+ Assert.Contains("string", this._commonHookData.Keys);
+ }
+
+ [Fact]
+ public void HookData_Has_Values()
+ {
+ Assert.Equal(5, this._commonHookData.Values.Count);
+ Assert.Contains(true, this._commonHookData.Values);
+ Assert.Contains(1, this._commonHookData.Values);
+ Assert.Contains(1.2f, this._commonHookData.Values);
+ Assert.Contains(1.2, this._commonHookData.Values);
+ Assert.Contains("string", this._commonHookData.Values);
+ }
+
+ [Fact]
+ public void HookData_Can_Be_Converted_To_Dictionary()
+ {
+ var asDictionary = this._commonHookData.AsDictionary();
+ Assert.Equal(5, asDictionary.Count);
+ Assert.Equal(true, asDictionary["bool"]);
+ Assert.Equal(1.2, asDictionary["double"]);
+ Assert.Equal(1.2f, asDictionary["float"]);
+ Assert.Equal(1, asDictionary["int"]);
+ Assert.Equal("string", asDictionary["string"]);
+ }
+
+ [Fact]
+ public void HookData_Get_Should_Throw_When_Key_Not_Found()
+ {
+ var hookData = new HookData();
+
+ Assert.Throws(() => hookData.Get("nonexistent"));
+ }
+
+ [Fact]
+ public void HookData_Indexer_Should_Throw_When_Key_Not_Found()
+ {
+ var hookData = new HookData();
+
+ Assert.Throws(() => _ = hookData["nonexistent"]);
+ }
+}
diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs
index bbb4da3fd..ae53f6db4 100644
--- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs
+++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs
@@ -90,7 +90,7 @@ await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empt
}
[Fact]
- [Specification("4.1.1", "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`.")]
+ [Specification("4.1.1", "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, `default value`, and `hook data`.")]
public void Hook_Context_Should_Not_Allow_Nulls()
{
Assert.Throws(() =>
@@ -108,6 +108,19 @@ public void Hook_Context_Should_Not_Allow_Nulls()
Assert.Throws(() =>
new HookContext("test", Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null),
new Metadata(null), null));
+
+ Assert.Throws(() => new SharedHookContext("test", Structure.Empty, FlagValueType.Object,
+ new ClientMetadata(null, null), new Metadata(null)).ToHookContext(null));
+
+ Assert.Throws(() =>
+ new HookContext(null, EvaluationContext.Empty,
+ new HookData()));
+
+ Assert.Throws(() =>
+ new HookContext(
+ new SharedHookContext("test", Structure.Empty, FlagValueType.Object,
+ new ClientMetadata(null, null), new Metadata(null)), EvaluationContext.Empty,
+ null));
}
[Fact]
@@ -151,6 +164,95 @@ await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty,
_ = hook2.Received(1).BeforeAsync(Arg.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), Arg.Any>());
}
+ [Fact]
+ [Specification("4.1.5", "The `hook data` MUST be mutable.")]
+ public async Task HookData_Must_Be_Mutable()
+ {
+ var hook = Substitute.For();
+
+ hook.BeforeAsync(Arg.Any>(), Arg.Any>())
+ .Returns(EvaluationContext.Empty).AndDoes(info =>
+ {
+ info.Arg>().Data.Set("test-a", true);
+ });
+ hook.AfterAsync(Arg.Any>(), Arg.Any>(),
+ Arg.Any>()).Returns(new ValueTask()).AndDoes(info =>
+ {
+ info.Arg>().Data.Set("test-b", "test-value");
+ });
+
+ await Api.Instance.SetProviderAsync(new NoOpFeatureProvider());
+ var client = Api.Instance.GetClient("test", "1.0.0");
+
+ await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty,
+ new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty));
+
+ _ = hook.Received(1).AfterAsync(Arg.Is>(hookContext =>
+ (bool)hookContext.Data.Get("test-a") == true
+ ), Arg.Any>(), Arg.Any>());
+ _ = hook.Received(1).FinallyAsync(Arg.Is>(hookContext =>
+ (bool)hookContext.Data.Get("test-a") == true && (string)hookContext.Data.Get("test-b") == "test-value"
+ ), Arg.Any>(), Arg.Any>());
+ }
+
+ [Fact]
+ [Specification("4.3.2",
+ "`Hook data` **MUST** must be created before the first `stage` invoked in a hook for a specific evaluation and propagated between each `stage` of the hook. The hook data is not shared between different hooks.")]
+ public async Task HookData_Must_Be_Unique_Per_Hook()
+ {
+ var hook1 = Substitute.For();
+ var hook2 = Substitute.For();
+
+ hook1.BeforeAsync(Arg.Any>(), Arg.Any>())
+ .Returns(EvaluationContext.Empty).AndDoes(info =>
+ {
+ info.Arg>().Data.Set("hook-1-value-a", true);
+ info.Arg>().Data.Set("same", true);
+ });
+ hook1.AfterAsync(Arg.Any>(), Arg.Any>(),
+ Arg.Any>()).Returns(new ValueTask()).AndDoes(info =>
+ {
+ info.Arg>().Data.Set("hook-1-value-b", "test-value-hook-1");
+ });
+
+ hook2.BeforeAsync(Arg.Any>(), Arg.Any>())
+ .Returns(EvaluationContext.Empty).AndDoes(info =>
+ {
+ info.Arg>().Data.Set("hook-2-value-a", false);
+ info.Arg>().Data.Set("same", false);
+ });
+ hook2.AfterAsync(Arg.Any>(), Arg.Any>(),
+ Arg.Any>()).Returns(new ValueTask()).AndDoes(info =>
+ {
+ info.Arg>().Data.Set("hook-2-value-b", "test-value-hook-2");
+ });
+
+ await Api.Instance.SetProviderAsync(new NoOpFeatureProvider());
+ var client = Api.Instance.GetClient("test", "1.0.0");
+
+ await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty,
+ new FlagEvaluationOptions(ImmutableList.Create(hook1, hook2),
+ ImmutableDictionary.Empty));
+
+ _ = hook1.Received(1).AfterAsync(Arg.Is>(hookContext =>
+ (bool)hookContext.Data.Get("hook-1-value-a") == true && (bool)hookContext.Data.Get("same") == true
+ ), Arg.Any>(), Arg.Any>());
+ _ = hook1.Received(1).FinallyAsync(Arg.Is>(hookContext =>
+ (bool)hookContext.Data.Get("hook-1-value-a") == true &&
+ (bool)hookContext.Data.Get("same") == true &&
+ (string)hookContext.Data.Get("hook-1-value-b") == "test-value-hook-1" && hookContext.Data.Count == 3
+ ), Arg.Any>(), Arg.Any>());
+
+ _ = hook2.Received(1).AfterAsync(Arg.Is>(hookContext =>
+ (bool)hookContext.Data.Get("hook-2-value-a") == false && (bool)hookContext.Data.Get("same") == false
+ ), Arg.Any>(), Arg.Any>());
+ _ = hook2.Received(1).FinallyAsync(Arg.Is>(hookContext =>
+ (bool)hookContext.Data.Get("hook-2-value-a") == false &&
+ (bool)hookContext.Data.Get("same") == false &&
+ (string)hookContext.Data.Get("hook-2-value-b") == "test-value-hook-2" && hookContext.Data.Count == 3
+ ), Arg.Any>(), Arg.Any>());
+ }
+
[Fact]
[Specification("3.2.2", "Evaluation context MUST be merged in the order: API (global; lowest precedence) - client - invocation - before hooks (highest precedence), with duplicate values being overwritten.")]
[Specification("4.3.4", "When `before` hooks have finished executing, any resulting `evaluation context` MUST be merged with the existing `evaluation context`.")]
@@ -394,7 +496,7 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination()
hook1.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty);
hook2.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty);
featureProvider1.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Throws(new Exception());
- hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask());
+ hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null).Throws(new Exception());
hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask());
await Api.Instance.SetProviderAsync(featureProvider1);
From d0bf40b9b40adc57a2a008a9497098b3cd1a05a7 Mon Sep 17 00:00:00 2001
From: Todd Baert
Date: Tue, 22 Apr 2025 14:24:23 -0400
Subject: [PATCH 007/126] chore: update release permissions
Signed-off-by: Todd Baert
---
.github/workflows/release.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a0fbad85c..7a17569f9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,7 +6,8 @@ on:
- main
permissions:
- contents: read
+ contents: write
+ pull-requests: write
jobs:
release-please:
From 8ecf50db2cab3a266de5c6c5216714570cfc6a52 Mon Sep 17 00:00:00 2001
From: Kyle <38759683+kylejuliandev@users.noreply.github.com>
Date: Wed, 23 Apr 2025 02:11:50 +0100
Subject: [PATCH 008/126] refactor: InMemoryProvider throwing when types
mismatched (#442)
## This PR
- Update InMemoryProvider to return an ErrorType with a default value
instead of throwing exceptions
- Add unit test to cover the new behavior
### Related Issues
Fixes #441
### Notes
### Follow-up Tasks
### How to test
Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com>
---
src/OpenFeature/Providers/Memory/InMemoryProvider.cs | 3 +--
.../Providers/Memory/InMemoryProviderTests.cs | 9 +++++++--
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs
index f95a0a06a..2eec879d0 100644
--- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs
+++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs
@@ -3,7 +3,6 @@
using System.Threading;
using System.Threading.Tasks;
using OpenFeature.Constant;
-using OpenFeature.Error;
using OpenFeature.Model;
namespace OpenFeature.Providers.Memory
@@ -112,7 +111,7 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati
return value.Evaluate(flagKey, defaultValue, context);
}
- throw new TypeMismatchException($"flag {flagKey} is not of type {typeof(T)}");
+ return new ResolutionDetails(flagKey, defaultValue, ErrorType.TypeMismatch, Reason.Error);
}
}
}
diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs
index 7a174fc51..c575dc56c 100644
--- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs
+++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs
@@ -186,9 +186,14 @@ public async Task MissingFlag_ShouldReturnFlagNotFoundEvaluationFlag()
}
[Fact]
- public async Task MismatchedFlag_ShouldThrow()
+ public async Task MismatchedFlag_ShouldReturnTypeMismatchError()
{
- await Assert.ThrowsAsync(() => this.commonProvider.ResolveStringValueAsync("boolean-flag", "nope", EvaluationContext.Empty));
+ // Act
+ var result = await this.commonProvider.ResolveStringValueAsync("boolean-flag", "nope", EvaluationContext.Empty);
+
+ // Assert
+ Assert.Equal(Reason.Error, result.Reason);
+ Assert.Equal(ErrorType.TypeMismatch, result.ErrorType);
}
[Fact]
From 1acc00fa7a6a38152d97fd7efc9f7e8befb1c3ed Mon Sep 17 00:00:00 2001
From: Todd Baert
Date: Wed, 23 Apr 2025 10:15:20 -0400
Subject: [PATCH 009/126] chore: packages read in release please
Signed-off-by: Todd Baert
---
.github/workflows/release.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7a17569f9..3702d88bd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,6 +14,8 @@ jobs:
permissions:
contents: write # for googleapis/release-please-action to create release commit
pull-requests: write # for googleapis/release-please-action to create release PR
+ packages: read # for internal nuget reading
+
runs-on: ubuntu-latest
steps:
From dfecd0c6a4467e5c1afe481e785e3e0f179beb25 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 23 Apr 2025 17:33:01 +0100
Subject: [PATCH 010/126] chore(deps): update github/codeql-action digest to
28deaed (#446)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
|
[github/codeql-action](https://github.com/github/codeql-action)
| action | digest | `45775bd` -> `28deaed` |
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/open-feature/dotnet-sdk).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/codeql-analysis.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 57ca46418..d943ef016 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -42,7 +42,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3
+ uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3
+ uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3
+ uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3
From 858b286dba2313239141c20ec6770504d340fbe0 Mon Sep 17 00:00:00 2001
From: Kyle <38759683+kylejuliandev@users.noreply.github.com>
Date: Wed, 23 Apr 2025 21:32:24 +0100
Subject: [PATCH 011/126] docs: update documentation on SetProviderAsync (#449)
## This PR
- Clarifies the behaviour of SetProviderAsync. Exceptions can be thrown
by a provider during initialization.
### Related Issues
Fixes #445
### Notes
### Follow-up Tasks
### How to test
Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com>
---
README.md | 48 ++++++++++++++++++++++++++++++++++--------
src/OpenFeature/Api.cs | 4 +++-
2 files changed, 42 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 7a2261772..b3d6ae985 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,14 @@ dotnet add package OpenFeature
public async Task Example()
{
// Register your feature flag provider
- await Api.Instance.SetProviderAsync(new InMemoryProvider());
+ try
+ {
+ await Api.Instance.SetProviderAsync(new InMemoryProvider());
+ }
+ catch (Exception ex)
+ {
+ // Log error
+ }
// Create a new client
FeatureClient client = Api.Instance.GetClient();
@@ -63,7 +70,7 @@ public async Task Example()
if ( v2Enabled )
{
- //Do some work
+ // Do some work
}
}
```
@@ -96,9 +103,18 @@ If the provider you're looking for hasn't been created yet, see the [develop a p
Once you've added a provider as a dependency, it can be registered with OpenFeature like this:
```csharp
-await Api.Instance.SetProviderAsync(new MyProvider());
+try
+{
+ await Api.Instance.SetProviderAsync(new MyProvider());
+}
+catch (Exception ex)
+{
+ // Log error
+}
```
+When calling `SetProviderAsync` an exception may be thrown if the provider cannot be initialized. This may occur if the provider has not been configured correctly. See the documentation for the provider you are using for more information on how to configure the provider correctly.
+
In some situations, it may be beneficial to register multiple providers in the same application.
This is possible using [domains](#domains), which is covered in more detail below.
@@ -177,11 +193,18 @@ A domain is a logical identifier which can be used to associate clients with a p
If a domain has no associated provider, the default provider is used.
```csharp
-// registering the default provider
-await Api.Instance.SetProviderAsync(new LocalProvider());
+try
+{
+ // registering the default provider
+ await Api.Instance.SetProviderAsync(new LocalProvider());
-// registering a provider to a domain
-await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider());
+ // registering a provider to a domain
+ await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider());
+}
+catch (Exception ex)
+{
+ // Log error
+}
// a client backed by default provider
FeatureClient clientDefault = Api.Instance.GetClient();
@@ -224,8 +247,15 @@ EventHandlerDelegate callback = EventHandler;
var myClient = Api.Instance.GetClient("my-client");
-var provider = new ExampleProvider();
-await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider);
+try
+{
+ var provider = new ExampleProvider();
+ await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider);
+}
+catch (Exception ex)
+{
+ // Log error
+}
myClient.AddHandler(ProviderEventTypes.ProviderReady, callback);
```
diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs
index 703218835..1f52a2a1f 100644
--- a/src/OpenFeature/Api.cs
+++ b/src/OpenFeature/Api.cs
@@ -43,7 +43,7 @@ private Api() { }
/// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete,
/// await the returned task.
///
- /// The provider cannot be set to null. Attempting to set the provider to null has no effect.
+ /// The provider cannot be set to null. Attempting to set the provider to null has no effect. May throw an exception if cannot be initialized.
/// Implementation of
public async Task SetProviderAsync(FeatureProvider featureProvider)
{
@@ -56,8 +56,10 @@ public async Task SetProviderAsync(FeatureProvider featureProvider)
/// Binds the feature provider to the given domain. In order to wait for the provider to be set, and
/// initialization to complete, await the returned task.
///
+ /// The provider cannot be set to null. Attempting to set the provider to null has no effect. May throw an exception if cannot be initialized.
/// An identifier which logically binds clients with providers
/// Implementation of
+ /// domain cannot be null or empty
public async Task SetProviderAsync(string domain, FeatureProvider featureProvider)
{
if (string.IsNullOrWhiteSpace(domain))
From e162169af0b5518f12527a8601d6dfcdf379b4f7 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 23 Apr 2025 17:20:02 -0400
Subject: [PATCH 012/126] chore(deps): update spec digest to 36944c6 (#450)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| spec | digest | `18cde17` -> `36944c6` |
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/open-feature/dotnet-sdk).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
spec | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spec b/spec
index 18cde1708..36944c68d 160000
--- a/spec
+++ b/spec
@@ -1 +1 @@
-Subproject commit 18cde1708e08d4f380ba30b4b1649e06683edfd2
+Subproject commit 36944c68dd60e874661f5efd022ccafb9af76535
From 4795685bc8c557cedac551c3f5e9f7fd1da0d55e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Thu, 24 Apr 2025 17:45:55 +0100
Subject: [PATCH 013/126] ci: update renovate configuration to fix immortal PRs
(#451)
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>
## This PR
This pull request updates the Renovate configuration to enhance
dependency management workflows. The most notable changes include
enabling dependency dashboard approval and specifying that dependencies
should not be recreated automatically.
Changes to Renovate configuration:
*
[`renovate.json`](diffhunk://#diff-7b5c8955fc544a11b4b74eddb4115f9cc51c9cf162dbffa60d37eeed82a55a57L5-R7):
Enabled the `dependencyDashboardApproval` setting to require manual
approval for dependency updates.
*
[`renovate.json`](diffhunk://#diff-7b5c8955fc544a11b4b74eddb4115f9cc51c9cf162dbffa60d37eeed82a55a57L5-R7):
Added the `recreateWhen` setting with a value of `"never"` to prevent
automatic recreation of dependency updates.
### Related Issues
Fixes #448
### Notes
I followed Renovate's general recommendation. I also changed to require
dashboard manual approval because we don't want the PRs to pop up for
dotnet extensions (they should be closed since we need to publish the
lowest version compatible; see #426). So we should look into the
dashboard issue #92 to create the PRs for dependencies.
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
renovate.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/renovate.json b/renovate.json
index daaea0b51..151c402c1 100644
--- a/renovate.json
+++ b/renovate.json
@@ -2,5 +2,7 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>open-feature/community-tooling"
- ]
+ ],
+ "dependencyDashboardApproval": true,
+ "recreateWhen": "never"
}
From 1e74a04f2b76c128a09c95dfd0b06803f2ef77bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Thu, 24 Apr 2025 20:03:03 +0100
Subject: [PATCH 014/126] chore: Change file scoped namespaces and cleanup job
(#453)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## This PR
This pull request introduces a significant refactor to the codebase by
converting all namespace declarations to file-scoped namespaces and
updating the `.editorconfig` to enforce this style. Additionally, it
simplifies the structure of multiple files by removing unnecessary
closing braces for namespace blocks.
### Namespace Refactoring:
* Converted all namespace declarations in the `src/OpenFeature`
directory to file-scoped namespaces for consistency and improved
readability. This change affects multiple files, including `Api.cs`,
`Constants.cs`, `Error/*.cs`, `EventExecutor.cs`, and `Extension/*.cs`.
[[1]](diffhunk://#diff-dc150fbd3b7be797470374ee7e38c7ab8a31eb9b6b7a52345e05114c2d32a15eL12-R13)
[[2]](diffhunk://#diff-b0112a803c4d4783b4b829626b4970801bda0de261d9887d3d5f5de30eb49d4fL1-L9)
[[3]](diffhunk://#diff-08c95bef1c40daf926c6da5657e9333d4481a1e76b03c8749b87e58d9c711af3L3-R4)
[[4]](diffhunk://#diff-95f94f0d86864094b6a4ed191b7a08bc306c6b2f21eb061b7de319aa9fa19e3fL1-R2)
[[5]](diffhunk://#diff-591e2f16844f704f064cbe31b7427e66c6f816f421a970fc7ad6fe1ad3c4c220L1-R2)R1,
[[6]](diffhunk://#diff-04e7c0ae5d31a3d10742b06dd413c512ee002ce27ce7b3b39cca79598c381a68L3-R4)
[[7]](diffhunk://#diff-0c85dcfcfa3c97d37ee0ba4394ca6958a14c013ee0940aaea09cefd2686bf41fL1-R2)
[[8]](diffhunk://#diff-a7a382f3d0da52015d1b9e03802692a86c61e7b57580747f31fae37d8dcc5cd6L4-R5)
[[9]](diffhunk://#diff-b9fdde9b61e62d7c474069ea5fc2a43d123ab86d93cb9a6d0673254a64536722L5-R6)
[[10]](diffhunk://#diff-bb4aadb03ea44e3b6b74b83f12b2b1a258e13836bcf815f996addcd5537a478fL5-R6)
[[11]](diffhunk://#diff-89efe60a8b640cc303a38e5b01bc27ad40599e364ff2654a57b7e42138056cdaL5-R6)
[[12]](diffhunk://#diff-7a8cbfba7673f1b69d3d927f60eb4ab0aa548403a118fad9eb43b9a22875dc02L5-R6)
[[13]](diffhunk://#diff-48ae8447bc31a75ecf4bba768a7b68244fbbc9dd5480db9114d9c74fb10fe2efL5-R6)
[[14]](diffhunk://#diff-9a3b5bf7bf3351a6161fc6dd75830bfbaab7aca730cf3f0ae6cd120a76b2f1b1L5-R6)
[[15]](diffhunk://#diff-c4388b2e7252e2e3ac0967dbfdd4647a924cdfc54da229667a0db3613b243a7eL5-R6)
[[16]](diffhunk://#diff-44c88ae43caf99ee733ec911fa85454a96c57d07fc57d2fadd44e12cd7d31cd4L5-R6)
[[17]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL10-R11)
[[18]](diffhunk://#diff-f94bf65f426e13a14f798a8db21a5c9dd3306f5941bde1aba79af3e41421bfc0L5-R6)
[[19]](diffhunk://#diff-484832cfacaa02b6872a26cf5202843e96e0125281deca5798974879d9d609a0L3-R4)
### Code Style Enforcement:
* Updated `.editorconfig` to include a new rule
(`csharp_style_namespace_declarations = file_scoped:warning`) to enforce
the use of file-scoped namespaces.
### Simplification of Code Structure:
* Removed unnecessary closing braces for namespace blocks in all
affected files, as they are no longer required with file-scoped
namespaces. This change simplifies the code and reduces visual clutter.
[[1]](diffhunk://#diff-dc150fbd3b7be797470374ee7e38c7ab8a31eb9b6b7a52345e05114c2d32a15eL369)
[[2]](diffhunk://#diff-b0112a803c4d4783b4b829626b4970801bda0de261d9887d3d5f5de30eb49d4fL1-L9)
[[3]](diffhunk://#diff-08c95bef1c40daf926c6da5657e9333d4481a1e76b03c8749b87e58d9c711af3L56)
[[4]](diffhunk://#diff-95f94f0d86864094b6a4ed191b7a08bc306c6b2f21eb061b7de319aa9fa19e3fL25)
[[5]](diffhunk://#diff-591e2f16844f704f064cbe31b7427e66c6f816f421a970fc7ad6fe1ad3c4c220L28)
[[6]](diffhunk://#diff-04e7c0ae5d31a3d10742b06dd413c512ee002ce27ce7b3b39cca79598c381a68L36)
[[7]](diffhunk://#diff-0c85dcfcfa3c97d37ee0ba4394ca6958a14c013ee0940aaea09cefd2686bf41fL50)
[[8]](diffhunk://#diff-a7a382f3d0da52015d1b9e03802692a86c61e7b57580747f31fae37d8dcc5cd6L29)
[[9]](diffhunk://#diff-b9fdde9b61e62d7c474069ea5fc2a43d123ab86d93cb9a6d0673254a64536722L23)
[[10]](diffhunk://#diff-bb4aadb03ea44e3b6b74b83f12b2b1a258e13836bcf815f996addcd5537a478fL23)
[[11]](diffhunk://#diff-89efe60a8b640cc303a38e5b01bc27ad40599e364ff2654a57b7e42138056cdaL23)
[[12]](diffhunk://#diff-7a8cbfba7673f1b69d3d927f60eb4ab0aa548403a118fad9eb43b9a22875dc02L23)
[[13]](diffhunk://#diff-48ae8447bc31a75ecf4bba768a7b68244fbbc9dd5480db9114d9c74fb10fe2efL23)
[[14]](diffhunk://#diff-9a3b5bf7bf3351a6161fc6dd75830bfbaab7aca730cf3f0ae6cd120a76b2f1b1L23)
[[15]](diffhunk://#diff-c4388b2e7252e2e3ac0967dbfdd4647a924cdfc54da229667a0db3613b243a7eL23)
[[16]](diffhunk://#diff-44c88ae43caf99ee733ec911fa85454a96c57d07fc57d2fadd44e12cd7d31cd4L23)
[[17]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL356)
[[18]](diffhunk://#diff-f94bf65f426e13a14f798a8db21a5c9dd3306f5941bde1aba79af3e41421bfc0L16)
[[19]](diffhunk://#diff-484832cfacaa02b6872a26cf5202843e96e0125281deca5798974879d9d609a0L13)
### Related Issues
Fixes #447
### Notes
I ran the `dotnet format OpenFeature.sln` to clean up the code. As you
can imagine, this generates a big diff. I would advise enabling `Hide
Whitespace` to review this PR, since it will show the conversion from
nested to file namespaces.
---------
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.editorconfig | 3 +
src/OpenFeature/Api.cs | 613 ++++----
src/OpenFeature/Constant/Constants.cs | 13 +-
src/OpenFeature/Constant/ErrorType.cs | 101 +-
src/OpenFeature/Constant/EventType.cs | 41 +-
src/OpenFeature/Constant/FlagValueType.cs | 41 +-
src/OpenFeature/Constant/ProviderStatus.cs | 51 +-
src/OpenFeature/Constant/Reason.cs | 93 +-
.../Error/FeatureProviderException.cs | 39 +-
.../Error/FlagNotFoundException.cs | 25 +-
src/OpenFeature/Error/GeneralException.cs | 25 +-
.../Error/InvalidContextException.cs | 25 +-
src/OpenFeature/Error/ParseErrorException.cs | 25 +-
.../Error/ProviderFatalException.cs | 25 +-
.../Error/ProviderNotReadyException.cs | 25 +-
.../Error/TargetingKeyMissingException.cs | 25 +-
.../Error/TypeMismatchException.cs | 25 +-
src/OpenFeature/EventExecutor.cs | 465 +++---
src/OpenFeature/Extension/EnumExtensions.cs | 15 +-
.../Extension/ResolutionDetailsExtensions.cs | 13 +-
src/OpenFeature/FeatureProvider.cs | 257 ++--
src/OpenFeature/Hook.cs | 145 +-
src/OpenFeature/HookData.cs | 173 ++-
src/OpenFeature/HookRunner.cs | 265 ++--
src/OpenFeature/Hooks/LoggingHook.cs | 259 ++--
src/OpenFeature/IEventBus.cs | 33 +-
src/OpenFeature/IFeatureClient.cs | 325 +++--
src/OpenFeature/Model/ClientMetadata.cs | 33 +-
src/OpenFeature/Model/EvaluationContext.cs | 201 ++-
.../Model/EvaluationContextBuilder.cs | 269 ++--
.../Model/FlagEvaluationDetails.cs | 123 +-
.../Model/FlagEvaluationOptions.cs | 67 +-
src/OpenFeature/Model/HookContext.cs | 147 +-
src/OpenFeature/Model/Metadata.cs | 31 +-
src/OpenFeature/Model/ProviderEvents.cs | 71 +-
src/OpenFeature/Model/ResolutionDetails.cs | 121 +-
src/OpenFeature/Model/Structure.cs | 225 ++-
src/OpenFeature/Model/StructureBuilder.cs | 249 ++--
.../Model/TrackingEventDetailsBuilder.cs | 275 ++--
src/OpenFeature/Model/Value.cs | 353 +++--
src/OpenFeature/NoOpProvider.cs | 85 +-
src/OpenFeature/OpenFeatureClient.cs | 571 ++++----
src/OpenFeature/ProviderRepository.cs | 439 +++---
src/OpenFeature/Providers/Memory/Flag.cs | 109 +-
.../Providers/Memory/InMemoryProvider.cs | 171 ++-
src/OpenFeature/SharedHookContext.cs | 91 +-
.../OpenFeatureClientBenchmarks.cs | 185 ++-
test/OpenFeature.Benchmarks/Program.cs | 11 +-
.../FeatureProviderExceptionTests.cs | 95 +-
.../OpenFeature.Tests/FeatureProviderTests.cs | 249 ++--
.../Hooks/LoggingHookTests.cs | 1247 ++++++++---------
.../Internal/SpecificationAttribute.cs | 21 +-
.../OpenFeatureClientTests.cs | 1155 ++++++++-------
.../OpenFeatureEvaluationContextTests.cs | 379 +++--
.../OpenFeatureEventTests.cs | 915 ++++++------
.../OpenFeature.Tests/OpenFeatureHookTests.cs | 1247 ++++++++---------
test/OpenFeature.Tests/OpenFeatureTests.cs | 571 ++++----
.../ProviderRepositoryTests.cs | 695 +++++----
.../Providers/Memory/InMemoryProviderTests.cs | 447 +++---
test/OpenFeature.Tests/StructureTests.cs | 193 ++-
test/OpenFeature.Tests/TestImplementations.cs | 231 ++-
test/OpenFeature.Tests/TestUtilsTest.cs | 25 +-
test/OpenFeature.Tests/ValueTests.cs | 373 +++--
63 files changed, 7378 insertions(+), 7437 deletions(-)
diff --git a/.editorconfig b/.editorconfig
index 7297b04dc..f6763f4da 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -148,6 +148,9 @@ dotnet_diagnostic.RS0041.severity = suggestion
# CA2007: Do not directly await a Task
dotnet_diagnostic.CA2007.severity = error
+# IDE0161: Convert to file-scoped namespace
+csharp_style_namespace_declarations = file_scoped:warning
+
[obj/**.cs]
generated_code = true
diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs
index 1f52a2a1f..cc0161c10 100644
--- a/src/OpenFeature/Api.cs
+++ b/src/OpenFeature/Api.cs
@@ -9,361 +9,360 @@
using OpenFeature.Error;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+///
+/// The evaluation API allows for the evaluation of feature flag values, independent of any flag control plane or vendor.
+/// In the absence of a provider the evaluation API uses the "No-op provider", which simply returns the supplied default flag value.
+///
+///
+public sealed class Api : IEventBus
{
+ private EvaluationContext _evaluationContext = EvaluationContext.Empty;
+ private EventExecutor _eventExecutor = new EventExecutor();
+ private ProviderRepository _repository = new ProviderRepository();
+ private readonly ConcurrentStack _hooks = new ConcurrentStack();
+ private ITransactionContextPropagator _transactionContextPropagator = new NoOpTransactionContextPropagator();
+ private readonly object _transactionContextPropagatorLock = new();
+
+ /// The reader/writer locks are not disposed because the singleton instance should never be disposed.
+ private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim();
+
+ ///
+ /// Singleton instance of Api
+ ///
+ public static Api Instance { get; private set; } = new Api();
+
+ // Explicit static constructor to tell C# compiler
+ // not to mark type as beforeFieldInit
+ // IE Lazy way of ensuring this is thread safe without using locks
+ static Api() { }
+ private Api() { }
+
///
- /// The evaluation API allows for the evaluation of feature flag values, independent of any flag control plane or vendor.
- /// In the absence of a provider the evaluation API uses the "No-op provider", which simply returns the supplied default flag value.
+ /// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete,
+ /// await the returned task.
///
- ///
- public sealed class Api : IEventBus
+ /// The provider cannot be set to null. Attempting to set the provider to null has no effect. May throw an exception if cannot be initialized.
+ /// Implementation of
+ public async Task SetProviderAsync(FeatureProvider featureProvider)
{
- private EvaluationContext _evaluationContext = EvaluationContext.Empty;
- private EventExecutor _eventExecutor = new EventExecutor();
- private ProviderRepository _repository = new ProviderRepository();
- private readonly ConcurrentStack _hooks = new ConcurrentStack();
- private ITransactionContextPropagator _transactionContextPropagator = new NoOpTransactionContextPropagator();
- private readonly object _transactionContextPropagatorLock = new();
-
- /// The reader/writer locks are not disposed because the singleton instance should never be disposed.
- private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim();
-
- ///
- /// Singleton instance of Api
- ///
- public static Api Instance { get; private set; } = new Api();
-
- // Explicit static constructor to tell C# compiler
- // not to mark type as beforeFieldInit
- // IE Lazy way of ensuring this is thread safe without using locks
- static Api() { }
- private Api() { }
-
- ///
- /// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete,
- /// await the returned task.
- ///
- /// The provider cannot be set to null. Attempting to set the provider to null has no effect. May throw an exception if cannot be initialized.
- /// Implementation of
- public async Task SetProviderAsync(FeatureProvider featureProvider)
- {
- this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider);
- await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false);
+ this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider);
+ await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false);
- }
+ }
- ///
- /// Binds the feature provider to the given domain. In order to wait for the provider to be set, and
- /// initialization to complete, await the returned task.
- ///
- /// The provider cannot be set to null. Attempting to set the provider to null has no effect. May throw an exception if cannot be initialized.
- /// An identifier which logically binds clients with providers
- /// Implementation of
- /// domain cannot be null or empty
- public async Task SetProviderAsync(string domain, FeatureProvider featureProvider)
+ ///
+ /// Binds the feature provider to the given domain. In order to wait for the provider to be set, and
+ /// initialization to complete, await the returned task.
+ ///
+ /// The provider cannot be set to null. Attempting to set the provider to null has no effect. May throw an exception if cannot be initialized.
+ /// An identifier which logically binds clients with providers
+ /// Implementation of
+ /// domain cannot be null or empty
+ public async Task SetProviderAsync(string domain, FeatureProvider featureProvider)
+ {
+ if (string.IsNullOrWhiteSpace(domain))
{
- if (string.IsNullOrWhiteSpace(domain))
- {
- throw new ArgumentNullException(nameof(domain));
- }
- this._eventExecutor.RegisterClientFeatureProvider(domain, featureProvider);
- await this._repository.SetProviderAsync(domain, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false);
+ throw new ArgumentNullException(nameof(domain));
}
+ this._eventExecutor.RegisterClientFeatureProvider(domain, featureProvider);
+ await this._repository.SetProviderAsync(domain, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false);
+ }
- ///
- /// Gets the feature provider
- ///
- /// The feature provider may be set from multiple threads, when accessing the global feature provider
- /// it should be accessed once for an operation, and then that reference should be used for all dependent
- /// operations. For instance, during an evaluation the flag resolution method, and the provider hooks
- /// should be accessed from the same reference, not two independent calls to
- /// .
- ///
- ///
- ///
- public FeatureProvider GetProvider()
- {
- return this._repository.GetProvider();
- }
+ ///
+ /// Gets the feature provider
+ ///
+ /// The feature provider may be set from multiple threads, when accessing the global feature provider
+ /// it should be accessed once for an operation, and then that reference should be used for all dependent
+ /// operations. For instance, during an evaluation the flag resolution method, and the provider hooks
+ /// should be accessed from the same reference, not two independent calls to
+ /// .
+ ///
+ ///
+ ///
+ public FeatureProvider GetProvider()
+ {
+ return this._repository.GetProvider();
+ }
- ///
- /// Gets the feature provider with given domain
- ///
- /// An identifier which logically binds clients with providers
- /// A provider associated with the given domain, if domain is empty or doesn't
- /// have a corresponding provider the default provider will be returned
- public FeatureProvider GetProvider(string domain)
- {
- return this._repository.GetProvider(domain);
- }
+ ///
+ /// Gets the feature provider with given domain
+ ///
+ /// An identifier which logically binds clients with providers
+ /// A provider associated with the given domain, if domain is empty or doesn't
+ /// have a corresponding provider the default provider will be returned
+ public FeatureProvider GetProvider(string domain)
+ {
+ return this._repository.GetProvider(domain);
+ }
- ///
- /// Gets providers metadata
- ///
- /// This method is not guaranteed to return the same provider instance that may be used during an evaluation
- /// in the case where the provider may be changed from another thread.
- /// For multiple dependent provider operations see .
- ///
- ///
- ///
- public Metadata? GetProviderMetadata() => this.GetProvider().GetMetadata();
-
- ///
- /// Gets providers metadata assigned to the given domain. If the domain has no provider
- /// assigned to it the default provider will be returned
- ///
- /// An identifier which logically binds clients with providers
- /// Metadata assigned to provider
- public Metadata? GetProviderMetadata(string domain) => this.GetProvider(domain).GetMetadata();
-
- ///
- /// Create a new instance of using the current provider
- ///
- /// Name of client
- /// Version of client
- /// Logger instance used by client
- /// Context given to this client
- ///
- public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null,
- EvaluationContext? context = null) =>
- new FeatureClient(() => this._repository.GetProvider(name), name, version, logger, context);
-
- ///
- /// Appends list of hooks to global hooks list
- ///
- /// The appending operation will be atomic.
- ///
- ///
- /// A list of
- public void AddHooks(IEnumerable hooks)
-#if NET7_0_OR_GREATER
- => this._hooks.PushRange(hooks as Hook[] ?? hooks.ToArray());
-#else
- {
- // See: https://github.com/dotnet/runtime/issues/62121
- if (hooks is Hook[] array)
- {
- if (array.Length > 0)
- this._hooks.PushRange(array);
+ ///
+ /// Gets providers metadata
+ ///
+ /// This method is not guaranteed to return the same provider instance that may be used during an evaluation
+ /// in the case where the provider may be changed from another thread.
+ /// For multiple dependent provider operations see .
+ ///
+ ///
+ ///
+ public Metadata? GetProviderMetadata() => this.GetProvider().GetMetadata();
- return;
- }
+ ///
+ /// Gets providers metadata assigned to the given domain. If the domain has no provider
+ /// assigned to it the default provider will be returned
+ ///
+ /// An identifier which logically binds clients with providers
+ /// Metadata assigned to provider
+ public Metadata? GetProviderMetadata(string domain) => this.GetProvider(domain).GetMetadata();
- array = hooks.ToArray();
+ ///
+ /// Create a new instance of using the current provider
+ ///
+ /// Name of client
+ /// Version of client
+ /// Logger instance used by client
+ /// Context given to this client
+ ///
+ public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null,
+ EvaluationContext? context = null) =>
+ new FeatureClient(() => this._repository.GetProvider(name), name, version, logger, context);
+ ///
+ /// Appends list of hooks to global hooks list
+ ///
+ /// The appending operation will be atomic.
+ ///
+ ///
+ /// A list of
+ public void AddHooks(IEnumerable hooks)
+#if NET7_0_OR_GREATER
+ => this._hooks.PushRange(hooks as Hook[] ?? hooks.ToArray());
+#else
+ {
+ // See: https://github.com/dotnet/runtime/issues/62121
+ if (hooks is Hook[] array)
+ {
if (array.Length > 0)
this._hooks.PushRange(array);
+
+ return;
}
+
+ array = hooks.ToArray();
+
+ if (array.Length > 0)
+ this._hooks.PushRange(array);
+ }
#endif
- ///
- /// Adds a hook to global hooks list
- ///
- /// Hooks which are dependent on each other should be provided in a collection
- /// using the .
- ///
- ///
- /// Hook that implements the interface
- public void AddHooks(Hook hook) => this._hooks.Push(hook);
-
- ///
- /// Enumerates the global hooks.
- ///
- /// The items enumerated will reflect the registered hooks
- /// at the start of enumeration. Hooks added during enumeration
- /// will not be included.
- ///
- ///
- /// Enumeration of
- public IEnumerable GetHooks() => this._hooks.Reverse();
-
- ///
- /// Removes all hooks from global hooks list
- ///
- public void ClearHooks() => this._hooks.Clear();
-
- ///
- /// Sets the global
- ///
- /// The to set
- public void SetContext(EvaluationContext? context)
- {
- this._evaluationContextLock.EnterWriteLock();
- try
- {
- this._evaluationContext = context ?? EvaluationContext.Empty;
- }
- finally
- {
- this._evaluationContextLock.ExitWriteLock();
- }
- }
+ ///
+ /// Adds a hook to global hooks list
+ ///
+ /// Hooks which are dependent on each other should be provided in a collection
+ /// using the .
+ ///
+ ///
+ /// Hook that implements the interface
+ public void AddHooks(Hook hook) => this._hooks.Push(hook);
- ///
- /// Gets the global
- ///
- /// The evaluation context may be set from multiple threads, when accessing the global evaluation context
- /// it should be accessed once for an operation, and then that reference should be used for all dependent
- /// operations.
- ///
- ///
- /// An
- public EvaluationContext GetContext()
- {
- this._evaluationContextLock.EnterReadLock();
- try
- {
- return this._evaluationContext;
- }
- finally
- {
- this._evaluationContextLock.ExitReadLock();
- }
- }
+ ///
+ /// Enumerates the global hooks.
+ ///
+ /// The items enumerated will reflect the registered hooks
+ /// at the start of enumeration. Hooks added during enumeration
+ /// will not be included.
+ ///
+ ///
+ /// Enumeration of
+ public IEnumerable GetHooks() => this._hooks.Reverse();
- ///
- /// Return the transaction context propagator.
- ///
- /// the registered transaction context propagator
- internal ITransactionContextPropagator GetTransactionContextPropagator()
+ ///
+ /// Removes all hooks from global hooks list
+ ///
+ public void ClearHooks() => this._hooks.Clear();
+
+ ///
+ /// Sets the global
+ ///
+ /// The to set
+ public void SetContext(EvaluationContext? context)
+ {
+ this._evaluationContextLock.EnterWriteLock();
+ try
{
- return this._transactionContextPropagator;
+ this._evaluationContext = context ?? EvaluationContext.Empty;
}
-
- ///
- /// Sets the transaction context propagator.
- ///
- /// the transaction context propagator to be registered
- /// Transaction context propagator cannot be null
- public void SetTransactionContextPropagator(ITransactionContextPropagator transactionContextPropagator)
+ finally
{
- if (transactionContextPropagator == null)
- {
- throw new ArgumentNullException(nameof(transactionContextPropagator),
- "Transaction context propagator cannot be null");
- }
-
- lock (this._transactionContextPropagatorLock)
- {
- this._transactionContextPropagator = transactionContextPropagator;
- }
+ this._evaluationContextLock.ExitWriteLock();
}
+ }
- ///
- /// Returns the currently defined transaction context using the registered transaction context propagator.
- ///
- /// The current transaction context
- public EvaluationContext GetTransactionContext()
+ ///
+ /// Gets the global
+ ///
+ /// The evaluation context may be set from multiple threads, when accessing the global evaluation context
+ /// it should be accessed once for an operation, and then that reference should be used for all dependent
+ /// operations.
+ ///
+ ///
+ /// An
+ public EvaluationContext GetContext()
+ {
+ this._evaluationContextLock.EnterReadLock();
+ try
{
- return this._transactionContextPropagator.GetTransactionContext();
+ return this._evaluationContext;
}
-
- ///
- /// Sets the transaction context using the registered transaction context propagator.
- ///
- /// The to set
- /// Transaction context propagator is not set.
- /// Evaluation context cannot be null
- public void SetTransactionContext(EvaluationContext evaluationContext)
+ finally
{
- if (evaluationContext == null)
- {
- throw new ArgumentNullException(nameof(evaluationContext), "Evaluation context cannot be null");
- }
-
- this._transactionContextPropagator.SetTransactionContext(evaluationContext);
+ this._evaluationContextLock.ExitReadLock();
}
+ }
- ///
- ///
- /// Shut down and reset the current status of OpenFeature API.
- ///
- ///
- /// This call cleans up all active providers and attempts to shut down internal event handling mechanisms.
- /// Once shut down is complete, API is reset and ready to use again.
- ///
- ///
- public async Task ShutdownAsync()
+ ///
+ /// Return the transaction context propagator.
+ ///
+ /// the registered transaction context propagator
+ internal ITransactionContextPropagator GetTransactionContextPropagator()
+ {
+ return this._transactionContextPropagator;
+ }
+
+ ///
+ /// Sets the transaction context propagator.
+ ///
+ /// the transaction context propagator to be registered
+ /// Transaction context propagator cannot be null
+ public void SetTransactionContextPropagator(ITransactionContextPropagator transactionContextPropagator)
+ {
+ if (transactionContextPropagator == null)
{
- await using (this._eventExecutor.ConfigureAwait(false))
- await using (this._repository.ConfigureAwait(false))
- {
- this._evaluationContext = EvaluationContext.Empty;
- this._hooks.Clear();
- this._transactionContextPropagator = new NoOpTransactionContextPropagator();
-
- // TODO: make these lazy to avoid extra allocations on the common cleanup path?
- this._eventExecutor = new EventExecutor();
- this._repository = new ProviderRepository();
- }
+ throw new ArgumentNullException(nameof(transactionContextPropagator),
+ "Transaction context propagator cannot be null");
}
- ///
- public void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ lock (this._transactionContextPropagatorLock)
{
- this._eventExecutor.AddApiLevelHandler(type, handler);
+ this._transactionContextPropagator = transactionContextPropagator;
}
+ }
+
+ ///
+ /// Returns the currently defined transaction context using the registered transaction context propagator.
+ ///
+ /// The current transaction context
+ public EvaluationContext GetTransactionContext()
+ {
+ return this._transactionContextPropagator.GetTransactionContext();
+ }
- ///
- public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ ///
+ /// Sets the transaction context using the registered transaction context propagator.
+ ///
+ /// The to set
+ /// Transaction context propagator is not set.
+ /// Evaluation context cannot be null
+ public void SetTransactionContext(EvaluationContext evaluationContext)
+ {
+ if (evaluationContext == null)
{
- this._eventExecutor.RemoveApiLevelHandler(type, handler);
+ throw new ArgumentNullException(nameof(evaluationContext), "Evaluation context cannot be null");
}
- ///
- /// Sets the logger for the API
- ///
- /// The logger to be used
- public void SetLogger(ILogger logger)
+ this._transactionContextPropagator.SetTransactionContext(evaluationContext);
+ }
+
+ ///
+ ///
+ /// Shut down and reset the current status of OpenFeature API.
+ ///
+ ///
+ /// This call cleans up all active providers and attempts to shut down internal event handling mechanisms.
+ /// Once shut down is complete, API is reset and ready to use again.
+ ///
+ ///
+ public async Task ShutdownAsync()
+ {
+ await using (this._eventExecutor.ConfigureAwait(false))
+ await using (this._repository.ConfigureAwait(false))
{
- this._eventExecutor.SetLogger(logger);
- this._repository.SetLogger(logger);
+ this._evaluationContext = EvaluationContext.Empty;
+ this._hooks.Clear();
+ this._transactionContextPropagator = new NoOpTransactionContextPropagator();
+
+ // TODO: make these lazy to avoid extra allocations on the common cleanup path?
+ this._eventExecutor = new EventExecutor();
+ this._repository = new ProviderRepository();
}
+ }
- internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
- => this._eventExecutor.AddClientHandler(client, eventType, handler);
+ ///
+ public void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ this._eventExecutor.AddApiLevelHandler(type, handler);
+ }
- internal void RemoveClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
- => this._eventExecutor.RemoveClientHandler(client, eventType, handler);
+ ///
+ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ this._eventExecutor.RemoveApiLevelHandler(type, handler);
+ }
- ///
- /// Update the provider state to READY and emit a READY event after successful init.
- ///
- private async Task AfterInitialization(FeatureProvider provider)
- {
- provider.Status = ProviderStatus.Ready;
- var eventPayload = new ProviderEventPayload
- {
- Type = ProviderEventTypes.ProviderReady,
- Message = "Provider initialization complete",
- ProviderName = provider.GetMetadata()?.Name,
- };
-
- await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
- }
+ ///
+ /// Sets the logger for the API
+ ///
+ /// The logger to be used
+ public void SetLogger(ILogger logger)
+ {
+ this._eventExecutor.SetLogger(logger);
+ this._repository.SetLogger(logger);
+ }
+
+ internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ => this._eventExecutor.AddClientHandler(client, eventType, handler);
- ///
- /// Update the provider state to ERROR and emit an ERROR after failed init.
- ///
- private async Task AfterError(FeatureProvider provider, Exception? ex)
+ internal void RemoveClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ => this._eventExecutor.RemoveClientHandler(client, eventType, handler);
+
+ ///
+ /// Update the provider state to READY and emit a READY event after successful init.
+ ///
+ private async Task AfterInitialization(FeatureProvider provider)
+ {
+ provider.Status = ProviderStatus.Ready;
+ var eventPayload = new ProviderEventPayload
{
- provider.Status = typeof(ProviderFatalException) == ex?.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error;
- var eventPayload = new ProviderEventPayload
- {
- Type = ProviderEventTypes.ProviderError,
- Message = $"Provider initialization error: {ex?.Message}",
- ProviderName = provider.GetMetadata()?.Name,
- };
-
- await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
- }
+ Type = ProviderEventTypes.ProviderReady,
+ Message = "Provider initialization complete",
+ ProviderName = provider.GetMetadata()?.Name,
+ };
+
+ await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
+ }
- ///
- /// This method should only be using for testing purposes. It will reset the singleton instance of the API.
- ///
- internal static void ResetApi()
+ ///
+ /// Update the provider state to ERROR and emit an ERROR after failed init.
+ ///
+ private async Task AfterError(FeatureProvider provider, Exception? ex)
+ {
+ provider.Status = typeof(ProviderFatalException) == ex?.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error;
+ var eventPayload = new ProviderEventPayload
{
- Instance = new Api();
- }
+ Type = ProviderEventTypes.ProviderError,
+ Message = $"Provider initialization error: {ex?.Message}",
+ ProviderName = provider.GetMetadata()?.Name,
+ };
+
+ await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
+ }
+
+ ///
+ /// This method should only be using for testing purposes. It will reset the singleton instance of the API.
+ ///
+ internal static void ResetApi()
+ {
+ Instance = new Api();
}
}
diff --git a/src/OpenFeature/Constant/Constants.cs b/src/OpenFeature/Constant/Constants.cs
index 0c58ec4d7..319844b88 100644
--- a/src/OpenFeature/Constant/Constants.cs
+++ b/src/OpenFeature/Constant/Constants.cs
@@ -1,9 +1,8 @@
-namespace OpenFeature.Constant
+namespace OpenFeature.Constant;
+
+internal static class NoOpProvider
{
- internal static class NoOpProvider
- {
- public const string NoOpProviderName = "No-op Provider";
- public const string ReasonNoOp = "No-op";
- public const string Variant = "No-op";
- }
+ public const string NoOpProviderName = "No-op Provider";
+ public const string ReasonNoOp = "No-op";
+ public const string Variant = "No-op";
}
diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs
index 4660e41a3..d36f3d963 100644
--- a/src/OpenFeature/Constant/ErrorType.cs
+++ b/src/OpenFeature/Constant/ErrorType.cs
@@ -1,56 +1,55 @@
using System.ComponentModel;
-namespace OpenFeature.Constant
+namespace OpenFeature.Constant;
+
+///
+/// These errors are used to indicate abnormal execution when evaluation a flag
+///
+///
+public enum ErrorType
{
///
- /// These errors are used to indicate abnormal execution when evaluation a flag
- ///
- ///
- public enum ErrorType
- {
- ///
- /// Default value, no error occured
- ///
- None,
-
- ///
- /// Provider has yet been initialized
- ///
- [Description("PROVIDER_NOT_READY")] ProviderNotReady,
-
- ///
- /// Provider was unable to find the flag
- ///
- [Description("FLAG_NOT_FOUND")] FlagNotFound,
-
- ///
- /// Provider failed to parse the flag response
- ///
- [Description("PARSE_ERROR")] ParseError,
-
- ///
- /// Request type does not match the expected type
- ///
- [Description("TYPE_MISMATCH")] TypeMismatch,
-
- ///
- /// Abnormal execution of the provider
- ///
- [Description("GENERAL")] General,
-
- ///
- /// Context does not satisfy provider requirements.
- ///
- [Description("INVALID_CONTEXT")] InvalidContext,
-
- ///
- /// Context does not contain a targeting key and the provider requires one.
- ///
- [Description("TARGETING_KEY_MISSING")] TargetingKeyMissing,
-
- ///
- /// The provider has entered an irrecoverable error state.
- ///
- [Description("PROVIDER_FATAL")] ProviderFatal,
- }
+ /// Default value, no error occured
+ ///
+ None,
+
+ ///
+ /// Provider has yet been initialized
+ ///
+ [Description("PROVIDER_NOT_READY")] ProviderNotReady,
+
+ ///
+ /// Provider was unable to find the flag
+ ///
+ [Description("FLAG_NOT_FOUND")] FlagNotFound,
+
+ ///
+ /// Provider failed to parse the flag response
+ ///
+ [Description("PARSE_ERROR")] ParseError,
+
+ ///
+ /// Request type does not match the expected type
+ ///
+ [Description("TYPE_MISMATCH")] TypeMismatch,
+
+ ///
+ /// Abnormal execution of the provider
+ ///
+ [Description("GENERAL")] General,
+
+ ///
+ /// Context does not satisfy provider requirements.
+ ///
+ [Description("INVALID_CONTEXT")] InvalidContext,
+
+ ///
+ /// Context does not contain a targeting key and the provider requires one.
+ ///
+ [Description("TARGETING_KEY_MISSING")] TargetingKeyMissing,
+
+ ///
+ /// The provider has entered an irrecoverable error state.
+ ///
+ [Description("PROVIDER_FATAL")] ProviderFatal,
}
diff --git a/src/OpenFeature/Constant/EventType.cs b/src/OpenFeature/Constant/EventType.cs
index 3d3c9dc89..369c10b2f 100644
--- a/src/OpenFeature/Constant/EventType.cs
+++ b/src/OpenFeature/Constant/EventType.cs
@@ -1,25 +1,24 @@
-namespace OpenFeature.Constant
+namespace OpenFeature.Constant;
+
+///
+/// The ProviderEventTypes enum represents the available event types of a provider.
+///
+public enum ProviderEventTypes
{
///
- /// The ProviderEventTypes enum represents the available event types of a provider.
+ /// ProviderReady should be emitted by a provider upon completing its initialisation.
///
- public enum ProviderEventTypes
- {
- ///
- /// ProviderReady should be emitted by a provider upon completing its initialisation.
- ///
- ProviderReady,
- ///
- /// ProviderError should be emitted by a provider upon encountering an error.
- ///
- ProviderError,
- ///
- /// ProviderConfigurationChanged should be emitted by a provider when a flag configuration has been changed.
- ///
- ProviderConfigurationChanged,
- ///
- /// ProviderStale should be emitted by a provider when it goes into the stale state.
- ///
- ProviderStale
- }
+ ProviderReady,
+ ///
+ /// ProviderError should be emitted by a provider upon encountering an error.
+ ///
+ ProviderError,
+ ///
+ /// ProviderConfigurationChanged should be emitted by a provider when a flag configuration has been changed.
+ ///
+ ProviderConfigurationChanged,
+ ///
+ /// ProviderStale should be emitted by a provider when it goes into the stale state.
+ ///
+ ProviderStale
}
diff --git a/src/OpenFeature/Constant/FlagValueType.cs b/src/OpenFeature/Constant/FlagValueType.cs
index 94a35d5b9..d63db7122 100644
--- a/src/OpenFeature/Constant/FlagValueType.cs
+++ b/src/OpenFeature/Constant/FlagValueType.cs
@@ -1,28 +1,27 @@
-namespace OpenFeature.Constant
+namespace OpenFeature.Constant;
+
+///
+/// Used to identity what object type of flag being evaluated
+///
+public enum FlagValueType
{
///
- /// Used to identity what object type of flag being evaluated
+ /// Flag is a boolean value
///
- public enum FlagValueType
- {
- ///
- /// Flag is a boolean value
- ///
- Boolean,
+ Boolean,
- ///
- /// Flag is a string value
- ///
- String,
+ ///
+ /// Flag is a string value
+ ///
+ String,
- ///
- /// Flag is a numeric value
- ///
- Number,
+ ///
+ /// Flag is a numeric value
+ ///
+ Number,
- ///
- /// Flag is a structured value
- ///
- Object
- }
+ ///
+ /// Flag is a structured value
+ ///
+ Object
}
diff --git a/src/OpenFeature/Constant/ProviderStatus.cs b/src/OpenFeature/Constant/ProviderStatus.cs
index 16dbd0247..760337463 100644
--- a/src/OpenFeature/Constant/ProviderStatus.cs
+++ b/src/OpenFeature/Constant/ProviderStatus.cs
@@ -1,36 +1,35 @@
using System.ComponentModel;
-namespace OpenFeature.Constant
+namespace OpenFeature.Constant;
+
+///
+/// The state of the provider.
+///
+///
+public enum ProviderStatus
{
///
- /// The state of the provider.
+ /// The provider has not been initialized and cannot yet evaluate flags.
///
- ///
- public enum ProviderStatus
- {
- ///
- /// The provider has not been initialized and cannot yet evaluate flags.
- ///
- [Description("NOT_READY")] NotReady,
+ [Description("NOT_READY")] NotReady,
- ///
- /// The provider is ready to resolve flags.
- ///
- [Description("READY")] Ready,
+ ///
+ /// The provider is ready to resolve flags.
+ ///
+ [Description("READY")] Ready,
- ///
- /// The provider's cached state is no longer valid and may not be up-to-date with the source of truth.
- ///
- [Description("STALE")] Stale,
+ ///
+ /// The provider's cached state is no longer valid and may not be up-to-date with the source of truth.
+ ///
+ [Description("STALE")] Stale,
- ///
- /// The provider is in an error state and unable to evaluate flags.
- ///
- [Description("ERROR")] Error,
+ ///
+ /// The provider is in an error state and unable to evaluate flags.
+ ///
+ [Description("ERROR")] Error,
- ///
- /// The provider has entered an irrecoverable error state.
- ///
- [Description("FATAL")] Fatal,
- }
+ ///
+ /// The provider has entered an irrecoverable error state.
+ ///
+ [Description("FATAL")] Fatal,
}
diff --git a/src/OpenFeature/Constant/Reason.cs b/src/OpenFeature/Constant/Reason.cs
index eac06c1e9..bd0653b50 100644
--- a/src/OpenFeature/Constant/Reason.cs
+++ b/src/OpenFeature/Constant/Reason.cs
@@ -1,50 +1,49 @@
-namespace OpenFeature.Constant
+namespace OpenFeature.Constant;
+
+///
+/// Common reasons used during flag resolution
+///
+/// Reason Specification
+public static class Reason
{
///
- /// Common reasons used during flag resolution
- ///
- /// Reason Specification
- public static class Reason
- {
- ///
- /// Use when the flag is matched based on the evaluation context user data
- ///
- public const string TargetingMatch = "TARGETING_MATCH";
-
- ///
- /// Use when the flag is matched based on a split rule in the feature flag provider
- ///
- public const string Split = "SPLIT";
-
- ///
- /// Use when the flag is disabled in the feature flag provider
- ///
- public const string Disabled = "DISABLED";
-
- ///
- /// Default reason when evaluating flag
- ///
- public const string Default = "DEFAULT";
-
- ///
- /// The resolved value is static (no dynamic evaluation)
- ///
- public const string Static = "STATIC";
-
- ///
- /// The resolved value was retrieved from cache
- ///
- public const string Cached = "CACHED";
-
- ///
- /// Use when an unknown reason is encountered when evaluating flag.
- /// An example of this is if the feature provider returns a reason that is not defined in the spec
- ///
- public const string Unknown = "UNKNOWN";
-
- ///
- /// Use this flag when abnormal execution is encountered.
- ///
- public const string Error = "ERROR";
- }
+ /// Use when the flag is matched based on the evaluation context user data
+ ///
+ public const string TargetingMatch = "TARGETING_MATCH";
+
+ ///
+ /// Use when the flag is matched based on a split rule in the feature flag provider
+ ///
+ public const string Split = "SPLIT";
+
+ ///
+ /// Use when the flag is disabled in the feature flag provider
+ ///
+ public const string Disabled = "DISABLED";
+
+ ///
+ /// Default reason when evaluating flag
+ ///
+ public const string Default = "DEFAULT";
+
+ ///
+ /// The resolved value is static (no dynamic evaluation)
+ ///
+ public const string Static = "STATIC";
+
+ ///
+ /// The resolved value was retrieved from cache
+ ///
+ public const string Cached = "CACHED";
+
+ ///
+ /// Use when an unknown reason is encountered when evaluating flag.
+ /// An example of this is if the feature provider returns a reason that is not defined in the spec
+ ///
+ public const string Unknown = "UNKNOWN";
+
+ ///
+ /// Use this flag when abnormal execution is encountered.
+ ///
+ public const string Error = "ERROR";
}
diff --git a/src/OpenFeature/Error/FeatureProviderException.cs b/src/OpenFeature/Error/FeatureProviderException.cs
index b2c43dc7a..b0431ab7b 100644
--- a/src/OpenFeature/Error/FeatureProviderException.cs
+++ b/src/OpenFeature/Error/FeatureProviderException.cs
@@ -1,29 +1,28 @@
using System;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Used to represent an abnormal error when evaluating a flag.
+/// This exception should be thrown when evaluating a flag inside a IFeatureFlag provider
+///
+public class FeatureProviderException : Exception
{
///
- /// Used to represent an abnormal error when evaluating a flag.
- /// This exception should be thrown when evaluating a flag inside a IFeatureFlag provider
+ /// Error that occurred during evaluation
///
- public class FeatureProviderException : Exception
- {
- ///
- /// Error that occurred during evaluation
- ///
- public ErrorType ErrorType { get; }
+ public ErrorType ErrorType { get; }
- ///
- /// Initialize a new instance of the class
- ///
- /// Common error types
- /// Exception message
- /// Optional inner exception
- public FeatureProviderException(ErrorType errorType, string? message = null, Exception? innerException = null)
- : base(message, innerException)
- {
- this.ErrorType = errorType;
- }
+ ///
+ /// Initialize a new instance of the class
+ ///
+ /// Common error types
+ /// Exception message
+ /// Optional inner exception
+ public FeatureProviderException(ErrorType errorType, string? message = null, Exception? innerException = null)
+ : base(message, innerException)
+ {
+ this.ErrorType = errorType;
}
}
diff --git a/src/OpenFeature/Error/FlagNotFoundException.cs b/src/OpenFeature/Error/FlagNotFoundException.cs
index b1a5b64a8..d685bb4a4 100644
--- a/src/OpenFeature/Error/FlagNotFoundException.cs
+++ b/src/OpenFeature/Error/FlagNotFoundException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Provider was unable to find the flag error when evaluating a flag.
+///
+[ExcludeFromCodeCoverage]
+public class FlagNotFoundException : FeatureProviderException
{
///
- /// Provider was unable to find the flag error when evaluating a flag.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class FlagNotFoundException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public FlagNotFoundException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.FlagNotFound, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public FlagNotFoundException(string? message = null, Exception? innerException = null)
- : base(ErrorType.FlagNotFound, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/Error/GeneralException.cs b/src/OpenFeature/Error/GeneralException.cs
index 4580ff319..0f9da24ca 100644
--- a/src/OpenFeature/Error/GeneralException.cs
+++ b/src/OpenFeature/Error/GeneralException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Abnormal execution of the provider when evaluating a flag.
+///
+[ExcludeFromCodeCoverage]
+public class GeneralException : FeatureProviderException
{
///
- /// Abnormal execution of the provider when evaluating a flag.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class GeneralException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public GeneralException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.General, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public GeneralException(string? message = null, Exception? innerException = null)
- : base(ErrorType.General, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/Error/InvalidContextException.cs b/src/OpenFeature/Error/InvalidContextException.cs
index ffea8ab17..881d0464f 100644
--- a/src/OpenFeature/Error/InvalidContextException.cs
+++ b/src/OpenFeature/Error/InvalidContextException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Context does not satisfy provider requirements when evaluating a flag.
+///
+[ExcludeFromCodeCoverage]
+public class InvalidContextException : FeatureProviderException
{
///
- /// Context does not satisfy provider requirements when evaluating a flag.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class InvalidContextException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public InvalidContextException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.InvalidContext, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public InvalidContextException(string? message = null, Exception? innerException = null)
- : base(ErrorType.InvalidContext, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/Error/ParseErrorException.cs b/src/OpenFeature/Error/ParseErrorException.cs
index 81ded4562..57bcf2719 100644
--- a/src/OpenFeature/Error/ParseErrorException.cs
+++ b/src/OpenFeature/Error/ParseErrorException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Provider failed to parse the flag response when evaluating a flag.
+///
+[ExcludeFromCodeCoverage]
+public class ParseErrorException : FeatureProviderException
{
///
- /// Provider failed to parse the flag response when evaluating a flag.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class ParseErrorException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public ParseErrorException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.ParseError, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public ParseErrorException(string? message = null, Exception? innerException = null)
- : base(ErrorType.ParseError, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/Error/ProviderFatalException.cs b/src/OpenFeature/Error/ProviderFatalException.cs
index 894a583dc..60ba5f251 100644
--- a/src/OpenFeature/Error/ProviderFatalException.cs
+++ b/src/OpenFeature/Error/ProviderFatalException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// An exception that signals the provider has entered an irrecoverable error state.
+///
+[ExcludeFromCodeCoverage]
+public class ProviderFatalException : FeatureProviderException
{
///
- /// An exception that signals the provider has entered an irrecoverable error state.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class ProviderFatalException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public ProviderFatalException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.ProviderFatal, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public ProviderFatalException(string? message = null, Exception? innerException = null)
- : base(ErrorType.ProviderFatal, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/Error/ProviderNotReadyException.cs b/src/OpenFeature/Error/ProviderNotReadyException.cs
index b66201d7f..5d2e3af18 100644
--- a/src/OpenFeature/Error/ProviderNotReadyException.cs
+++ b/src/OpenFeature/Error/ProviderNotReadyException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Provider has not yet been initialized when evaluating a flag.
+///
+[ExcludeFromCodeCoverage]
+public class ProviderNotReadyException : FeatureProviderException
{
///
- /// Provider has not yet been initialized when evaluating a flag.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class ProviderNotReadyException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public ProviderNotReadyException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.ProviderNotReady, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public ProviderNotReadyException(string? message = null, Exception? innerException = null)
- : base(ErrorType.ProviderNotReady, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/Error/TargetingKeyMissingException.cs b/src/OpenFeature/Error/TargetingKeyMissingException.cs
index 717424134..488009f41 100644
--- a/src/OpenFeature/Error/TargetingKeyMissingException.cs
+++ b/src/OpenFeature/Error/TargetingKeyMissingException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Context does not contain a targeting key and the provider requires one when evaluating a flag.
+///
+[ExcludeFromCodeCoverage]
+public class TargetingKeyMissingException : FeatureProviderException
{
///
- /// Context does not contain a targeting key and the provider requires one when evaluating a flag.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class TargetingKeyMissingException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public TargetingKeyMissingException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.TargetingKeyMissing, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public TargetingKeyMissingException(string? message = null, Exception? innerException = null)
- : base(ErrorType.TargetingKeyMissing, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/Error/TypeMismatchException.cs b/src/OpenFeature/Error/TypeMismatchException.cs
index 83ff0cf39..2df3b29f0 100644
--- a/src/OpenFeature/Error/TypeMismatchException.cs
+++ b/src/OpenFeature/Error/TypeMismatchException.cs
@@ -2,22 +2,21 @@
using System.Diagnostics.CodeAnalysis;
using OpenFeature.Constant;
-namespace OpenFeature.Error
+namespace OpenFeature.Error;
+
+///
+/// Request type does not match the expected type when evaluating a flag.
+///
+[ExcludeFromCodeCoverage]
+public class TypeMismatchException : FeatureProviderException
{
///
- /// Request type does not match the expected type when evaluating a flag.
+ /// Initialize a new instance of the class
///
- [ExcludeFromCodeCoverage]
- public class TypeMismatchException : FeatureProviderException
+ /// Exception message
+ /// Optional inner exception
+ public TypeMismatchException(string? message = null, Exception? innerException = null)
+ : base(ErrorType.TypeMismatch, message, innerException)
{
- ///
- /// Initialize a new instance of the class
- ///
- /// Exception message
- /// Optional inner exception
- public TypeMismatchException(string? message = null, Exception? innerException = null)
- : base(ErrorType.TypeMismatch, message, innerException)
- {
- }
}
}
diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs
index a1c1ddbdc..edb75780a 100644
--- a/src/OpenFeature/EventExecutor.cs
+++ b/src/OpenFeature/EventExecutor.cs
@@ -7,350 +7,349 @@
using OpenFeature.Constant;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+internal sealed partial class EventExecutor : IAsyncDisposable
{
- internal sealed partial class EventExecutor : IAsyncDisposable
- {
- private readonly object _lockObj = new();
- public readonly Channel EventChannel = Channel.CreateBounded(1);
- private FeatureProvider? _defaultProvider;
- private readonly Dictionary _namedProviderReferences = [];
- private readonly List _activeSubscriptions = [];
+ private readonly object _lockObj = new();
+ public readonly Channel EventChannel = Channel.CreateBounded(1);
+ private FeatureProvider? _defaultProvider;
+ private readonly Dictionary _namedProviderReferences = [];
+ private readonly List _activeSubscriptions = [];
- private readonly Dictionary> _apiHandlers = [];
- private readonly Dictionary>> _clientHandlers = [];
+ private readonly Dictionary> _apiHandlers = [];
+ private readonly Dictionary>> _clientHandlers = [];
- private ILogger _logger;
+ private ILogger _logger;
- public EventExecutor()
- {
- this._logger = NullLogger.Instance;
- Task.Run(this.ProcessEventAsync);
- }
+ public EventExecutor()
+ {
+ this._logger = NullLogger.Instance;
+ Task.Run(this.ProcessEventAsync);
+ }
- public ValueTask DisposeAsync() => new(this.ShutdownAsync());
+ public ValueTask DisposeAsync() => new(this.ShutdownAsync());
- internal void SetLogger(ILogger logger) => this._logger = logger;
+ internal void SetLogger(ILogger logger) => this._logger = logger;
- internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler)
+ internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
{
- lock (this._lockObj)
+ if (!this._apiHandlers.TryGetValue(eventType, out var eventHandlers))
{
- if (!this._apiHandlers.TryGetValue(eventType, out var eventHandlers))
- {
- eventHandlers = [];
- this._apiHandlers[eventType] = eventHandlers;
- }
+ eventHandlers = [];
+ this._apiHandlers[eventType] = eventHandlers;
+ }
- eventHandlers.Add(handler);
+ eventHandlers.Add(handler);
- this.EmitOnRegistration(this._defaultProvider, eventType, handler);
- }
+ this.EmitOnRegistration(this._defaultProvider, eventType, handler);
}
+ }
- internal void RemoveApiLevelHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ internal void RemoveApiLevelHandler(ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
{
- lock (this._lockObj)
+ if (this._apiHandlers.TryGetValue(type, out var eventHandlers))
{
- if (this._apiHandlers.TryGetValue(type, out var eventHandlers))
- {
- eventHandlers.Remove(handler);
- }
+ eventHandlers.Remove(handler);
}
}
+ }
- internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
{
- lock (this._lockObj)
+ // check if there is already a list of handlers for the given client and event type
+ if (!this._clientHandlers.TryGetValue(client, out var registry))
{
- // check if there is already a list of handlers for the given client and event type
- if (!this._clientHandlers.TryGetValue(client, out var registry))
- {
- registry = [];
- this._clientHandlers[client] = registry;
- }
+ registry = [];
+ this._clientHandlers[client] = registry;
+ }
- if (!this._clientHandlers[client].TryGetValue(eventType, out var eventHandlers))
- {
- eventHandlers = [];
- this._clientHandlers[client][eventType] = eventHandlers;
- }
+ if (!this._clientHandlers[client].TryGetValue(eventType, out var eventHandlers))
+ {
+ eventHandlers = [];
+ this._clientHandlers[client][eventType] = eventHandlers;
+ }
- this._clientHandlers[client][eventType].Add(handler);
+ this._clientHandlers[client][eventType].Add(handler);
- this.EmitOnRegistration(
- this._namedProviderReferences.TryGetValue(client, out var clientProviderReference)
- ? clientProviderReference
- : this._defaultProvider, eventType, handler);
- }
+ this.EmitOnRegistration(
+ this._namedProviderReferences.TryGetValue(client, out var clientProviderReference)
+ ? clientProviderReference
+ : this._defaultProvider, eventType, handler);
}
+ }
- internal void RemoveClientHandler(string client, ProviderEventTypes type, EventHandlerDelegate handler)
+ internal void RemoveClientHandler(string client, ProviderEventTypes type, EventHandlerDelegate handler)
+ {
+ lock (this._lockObj)
{
- lock (this._lockObj)
+ if (this._clientHandlers.TryGetValue(client, out var clientEventHandlers))
{
- if (this._clientHandlers.TryGetValue(client, out var clientEventHandlers))
+ if (clientEventHandlers.TryGetValue(type, out var eventHandlers))
{
- if (clientEventHandlers.TryGetValue(type, out var eventHandlers))
- {
- eventHandlers.Remove(handler);
- }
+ eventHandlers.Remove(handler);
}
}
}
+ }
- internal void RegisterDefaultFeatureProvider(FeatureProvider? provider)
+ internal void RegisterDefaultFeatureProvider(FeatureProvider? provider)
+ {
+ if (provider == null)
{
- if (provider == null)
- {
- return;
- }
- lock (this._lockObj)
- {
- var oldProvider = this._defaultProvider;
+ return;
+ }
+ lock (this._lockObj)
+ {
+ var oldProvider = this._defaultProvider;
- this._defaultProvider = provider;
+ this._defaultProvider = provider;
- this.StartListeningAndShutdownOld(this._defaultProvider, oldProvider);
- }
+ this.StartListeningAndShutdownOld(this._defaultProvider, oldProvider);
}
+ }
- internal void RegisterClientFeatureProvider(string client, FeatureProvider? provider)
+ internal void RegisterClientFeatureProvider(string client, FeatureProvider? provider)
+ {
+ if (provider == null)
+ {
+ return;
+ }
+ lock (this._lockObj)
{
- if (provider == null)
+ FeatureProvider? oldProvider = null;
+ if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider))
{
- return;
+ oldProvider = foundOldProvider;
}
- lock (this._lockObj)
- {
- FeatureProvider? oldProvider = null;
- if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider))
- {
- oldProvider = foundOldProvider;
- }
- this._namedProviderReferences[client] = provider;
+ this._namedProviderReferences[client] = provider;
- this.StartListeningAndShutdownOld(provider, oldProvider);
- }
+ this.StartListeningAndShutdownOld(provider, oldProvider);
}
+ }
- private void StartListeningAndShutdownOld(FeatureProvider newProvider, FeatureProvider? oldProvider)
+ private void StartListeningAndShutdownOld(FeatureProvider newProvider, FeatureProvider? oldProvider)
+ {
+ // check if the provider is already active - if not, we need to start listening for its emitted events
+ if (!this.IsProviderActive(newProvider))
{
- // check if the provider is already active - if not, we need to start listening for its emitted events
- if (!this.IsProviderActive(newProvider))
- {
- this._activeSubscriptions.Add(newProvider);
- Task.Run(() => this.ProcessFeatureProviderEventsAsync(newProvider));
- }
+ this._activeSubscriptions.Add(newProvider);
+ Task.Run(() => this.ProcessFeatureProviderEventsAsync(newProvider));
+ }
- if (oldProvider != null && !this.IsProviderBound(oldProvider))
- {
- this._activeSubscriptions.Remove(oldProvider);
- oldProvider.GetEventChannel().Writer.Complete();
- }
+ if (oldProvider != null && !this.IsProviderBound(oldProvider))
+ {
+ this._activeSubscriptions.Remove(oldProvider);
+ oldProvider.GetEventChannel().Writer.Complete();
}
+ }
- private bool IsProviderBound(FeatureProvider provider)
+ private bool IsProviderBound(FeatureProvider provider)
+ {
+ if (this._defaultProvider == provider)
+ {
+ return true;
+ }
+ foreach (var providerReference in this._namedProviderReferences.Values)
{
- if (this._defaultProvider == provider)
+ if (providerReference == provider)
{
return true;
}
- foreach (var providerReference in this._namedProviderReferences.Values)
- {
- if (providerReference == provider)
- {
- return true;
- }
- }
- return false;
}
+ return false;
+ }
+
+ private bool IsProviderActive(FeatureProvider providerRef)
+ {
+ return this._activeSubscriptions.Contains(providerRef);
+ }
- private bool IsProviderActive(FeatureProvider providerRef)
+ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ {
+ if (provider == null)
{
- return this._activeSubscriptions.Contains(providerRef);
+ return;
}
+ var status = provider.Status;
- private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes eventType, EventHandlerDelegate handler)
+ var message = status switch
{
- if (provider == null)
- {
- return;
- }
- var status = provider.Status;
+ ProviderStatus.Ready when eventType == ProviderEventTypes.ProviderReady => "Provider is ready",
+ ProviderStatus.Error when eventType == ProviderEventTypes.ProviderError => "Provider is in error state",
+ ProviderStatus.Stale when eventType == ProviderEventTypes.ProviderStale => "Provider is in stale state",
+ _ => string.Empty
+ };
- var message = status switch
- {
- ProviderStatus.Ready when eventType == ProviderEventTypes.ProviderReady => "Provider is ready",
- ProviderStatus.Error when eventType == ProviderEventTypes.ProviderError => "Provider is in error state",
- ProviderStatus.Stale when eventType == ProviderEventTypes.ProviderStale => "Provider is in stale state",
- _ => string.Empty
- };
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ return;
+ }
- if (string.IsNullOrWhiteSpace(message))
+ try
+ {
+ handler.Invoke(new ProviderEventPayload
{
- return;
- }
+ ProviderName = provider.GetMetadata()?.Name,
+ Type = eventType,
+ Message = message
+ });
+ }
+ catch (Exception exc)
+ {
+ this.ErrorRunningHandler(exc);
+ }
+ }
- try
- {
- handler.Invoke(new ProviderEventPayload
- {
- ProviderName = provider.GetMetadata()?.Name,
- Type = eventType,
- Message = message
- });
- }
- catch (Exception exc)
+ private async Task ProcessFeatureProviderEventsAsync(FeatureProvider provider)
+ {
+ if (provider.GetEventChannel() is not { Reader: { } reader })
+ {
+ return;
+ }
+
+ while (await reader.WaitToReadAsync().ConfigureAwait(false))
+ {
+ if (!reader.TryRead(out var item))
+ continue;
+
+ switch (item)
{
- this.ErrorRunningHandler(exc);
+ case ProviderEventPayload eventPayload:
+ UpdateProviderStatus(provider, eventPayload);
+ await this.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
+ break;
}
}
+ }
- private async Task ProcessFeatureProviderEventsAsync(FeatureProvider provider)
+ // Method to process events
+ private async Task ProcessEventAsync()
+ {
+ while (await this.EventChannel.Reader.WaitToReadAsync().ConfigureAwait(false))
{
- if (provider.GetEventChannel() is not { Reader: { } reader })
+ if (!this.EventChannel.Reader.TryRead(out var item))
{
- return;
+ continue;
}
- while (await reader.WaitToReadAsync().ConfigureAwait(false))
+ if (item is not Event e)
{
- if (!reader.TryRead(out var item))
- continue;
+ continue;
+ }
- switch (item)
- {
- case ProviderEventPayload eventPayload:
- UpdateProviderStatus(provider, eventPayload);
- await this.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
- break;
- }
+ lock (this._lockObj)
+ {
+ this.ProcessApiHandlers(e);
+ this.ProcessClientHandlers(e);
+ this.ProcessDefaultProviderHandlers(e);
}
}
+ }
- // Method to process events
- private async Task ProcessEventAsync()
+ private void ProcessApiHandlers(Event e)
+ {
+ if (e.EventPayload?.Type != null && this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers))
{
- while (await this.EventChannel.Reader.WaitToReadAsync().ConfigureAwait(false))
+ foreach (var eventHandler in eventHandlers)
{
- if (!this.EventChannel.Reader.TryRead(out var item))
- {
- continue;
- }
-
- if (item is not Event e)
- {
- continue;
- }
-
- lock (this._lockObj)
- {
- this.ProcessApiHandlers(e);
- this.ProcessClientHandlers(e);
- this.ProcessDefaultProviderHandlers(e);
- }
+ this.InvokeEventHandler(eventHandler, e);
}
}
+ }
- private void ProcessApiHandlers(Event e)
+ private void ProcessClientHandlers(Event e)
+ {
+ foreach (var keyAndValue in this._namedProviderReferences)
{
- if (e.EventPayload?.Type != null && this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers))
+ if (keyAndValue.Value == e.Provider
+ && this._clientHandlers.TryGetValue(keyAndValue.Key, out var clientRegistry)
+ && e.EventPayload?.Type != null
+ && clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers))
{
- foreach (var eventHandler in eventHandlers)
+ foreach (var eventHandler in clientEventHandlers)
{
this.InvokeEventHandler(eventHandler, e);
}
}
}
+ }
- private void ProcessClientHandlers(Event e)
+ private void ProcessDefaultProviderHandlers(Event e)
+ {
+ if (e.Provider != this._defaultProvider)
{
- foreach (var keyAndValue in this._namedProviderReferences)
- {
- if (keyAndValue.Value == e.Provider
- && this._clientHandlers.TryGetValue(keyAndValue.Key, out var clientRegistry)
- && e.EventPayload?.Type != null
- && clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers))
- {
- foreach (var eventHandler in clientEventHandlers)
- {
- this.InvokeEventHandler(eventHandler, e);
- }
- }
- }
+ return;
}
- private void ProcessDefaultProviderHandlers(Event e)
+ foreach (var keyAndValues in this._clientHandlers)
{
- if (e.Provider != this._defaultProvider)
+ if (this._namedProviderReferences.ContainsKey(keyAndValues.Key))
{
- return;
+ continue;
}
- foreach (var keyAndValues in this._clientHandlers)
+ if (e.EventPayload?.Type != null && keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers))
{
- if (this._namedProviderReferences.ContainsKey(keyAndValues.Key))
+ foreach (var eventHandler in clientEventHandlers)
{
- continue;
- }
-
- if (e.EventPayload?.Type != null && keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers))
- {
- foreach (var eventHandler in clientEventHandlers)
- {
- this.InvokeEventHandler(eventHandler, e);
- }
+ this.InvokeEventHandler(eventHandler, e);
}
}
}
+ }
- // map events to provider status as per spec: https://openfeature.dev/specification/sections/events/#requirement-535
- private static void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload eventPayload)
+ // map events to provider status as per spec: https://openfeature.dev/specification/sections/events/#requirement-535
+ private static void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload eventPayload)
+ {
+ switch (eventPayload.Type)
{
- switch (eventPayload.Type)
- {
- case ProviderEventTypes.ProviderReady:
- provider.Status = ProviderStatus.Ready;
- break;
- case ProviderEventTypes.ProviderStale:
- provider.Status = ProviderStatus.Stale;
- break;
- case ProviderEventTypes.ProviderError:
- provider.Status = eventPayload.ErrorType == ErrorType.ProviderFatal ? ProviderStatus.Fatal : ProviderStatus.Error;
- break;
- case ProviderEventTypes.ProviderConfigurationChanged:
- default: break;
- }
+ case ProviderEventTypes.ProviderReady:
+ provider.Status = ProviderStatus.Ready;
+ break;
+ case ProviderEventTypes.ProviderStale:
+ provider.Status = ProviderStatus.Stale;
+ break;
+ case ProviderEventTypes.ProviderError:
+ provider.Status = eventPayload.ErrorType == ErrorType.ProviderFatal ? ProviderStatus.Fatal : ProviderStatus.Error;
+ break;
+ case ProviderEventTypes.ProviderConfigurationChanged:
+ default: break;
}
+ }
- private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e)
+ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e)
+ {
+ try
{
- try
- {
- eventHandler.Invoke(e.EventPayload);
- }
- catch (Exception exc)
- {
- this.ErrorRunningHandler(exc);
- }
+ eventHandler.Invoke(e.EventPayload);
}
-
- public async Task ShutdownAsync()
+ catch (Exception exc)
{
- this.EventChannel.Writer.Complete();
- await this.EventChannel.Reader.Completion.ConfigureAwait(false);
+ this.ErrorRunningHandler(exc);
}
-
- [LoggerMessage(100, LogLevel.Error, "Error running handler")]
- partial void ErrorRunningHandler(Exception exception);
}
- internal class Event
+ public async Task ShutdownAsync()
{
- internal FeatureProvider? Provider { get; set; }
- internal ProviderEventPayload? EventPayload { get; set; }
+ this.EventChannel.Writer.Complete();
+ await this.EventChannel.Reader.Completion.ConfigureAwait(false);
}
+
+ [LoggerMessage(100, LogLevel.Error, "Error running handler")]
+ partial void ErrorRunningHandler(Exception exception);
+}
+
+internal class Event
+{
+ internal FeatureProvider? Provider { get; set; }
+ internal ProviderEventPayload? EventPayload { get; set; }
}
diff --git a/src/OpenFeature/Extension/EnumExtensions.cs b/src/OpenFeature/Extension/EnumExtensions.cs
index fe10afb5f..d5d7e72b9 100644
--- a/src/OpenFeature/Extension/EnumExtensions.cs
+++ b/src/OpenFeature/Extension/EnumExtensions.cs
@@ -2,15 +2,14 @@
using System.ComponentModel;
using System.Linq;
-namespace OpenFeature.Extension
+namespace OpenFeature.Extension;
+
+internal static class EnumExtensions
{
- internal static class EnumExtensions
+ public static string GetDescription(this Enum value)
{
- public static string GetDescription(this Enum value)
- {
- var field = value.GetType().GetField(value.ToString());
- var attribute = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
- return attribute?.Description ?? value.ToString();
- }
+ var field = value.GetType().GetField(value.ToString());
+ var attribute = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
+ return attribute?.Description ?? value.ToString();
}
}
diff --git a/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs
index f38356ad3..cf0d4f4aa 100644
--- a/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs
+++ b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs
@@ -1,13 +1,12 @@
using OpenFeature.Model;
-namespace OpenFeature.Extension
+namespace OpenFeature.Extension;
+
+internal static class ResolutionDetailsExtensions
{
- internal static class ResolutionDetailsExtensions
+ public static FlagEvaluationDetails ToFlagEvaluationDetails(this ResolutionDetails details)
{
- public static FlagEvaluationDetails ToFlagEvaluationDetails(this ResolutionDetails details)
- {
- return new FlagEvaluationDetails(details.FlagKey, details.Value, details.ErrorType, details.Reason,
- details.Variant, details.ErrorMessage, details.FlagMetadata);
- }
+ return new FlagEvaluationDetails(details.FlagKey, details.Value, details.ErrorType, details.Reason,
+ details.Variant, details.ErrorMessage, details.FlagMetadata);
}
}
diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs
index b5b9a30f7..9c9d93277 100644
--- a/src/OpenFeature/FeatureProvider.cs
+++ b/src/OpenFeature/FeatureProvider.cs
@@ -5,149 +5,148 @@
using OpenFeature.Constant;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+///
+/// The provider interface describes the abstraction layer for a feature flag provider.
+/// A provider acts as it translates layer between the generic feature flag structure to a target feature flag system.
+///
+/// Provider specification
+public abstract class FeatureProvider
{
///
- /// The provider interface describes the abstraction layer for a feature flag provider.
- /// A provider acts as it translates layer between the generic feature flag structure to a target feature flag system.
+ /// Gets an immutable list of hooks that belong to the provider.
+ /// By default, return an empty list
+ ///
+ /// Executed in the order of hooks
+ /// before: API, Client, Invocation, Provider
+ /// after: Provider, Invocation, Client, API
+ /// error (if applicable): Provider, Invocation, Client, API
+ /// finally: Provider, Invocation, Client, API
///
- /// Provider specification
- public abstract class FeatureProvider
- {
- ///
- /// Gets an immutable list of hooks that belong to the provider.
- /// By default, return an empty list
- ///
- /// Executed in the order of hooks
- /// before: API, Client, Invocation, Provider
- /// after: Provider, Invocation, Client, API
- /// error (if applicable): Provider, Invocation, Client, API
- /// finally: Provider, Invocation, Client, API
- ///
- /// Immutable list of hooks
- public virtual IImmutableList GetProviderHooks() => ImmutableList.Empty;
+ /// Immutable list of hooks
+ public virtual IImmutableList GetProviderHooks() => ImmutableList.Empty;
- ///
- /// The event channel of the provider.
- ///
- protected readonly Channel EventChannel = Channel.CreateBounded(1);
+ ///
+ /// The event channel of the provider.
+ ///
+ protected readonly Channel EventChannel = Channel.CreateBounded(1);
- ///
- /// Metadata describing the provider.
- ///
- ///
- public abstract Metadata? GetMetadata();
+ ///
+ /// Metadata describing the provider.
+ ///
+ ///
+ public abstract Metadata? GetMetadata();
- ///
- /// Resolves a boolean feature flag
- ///
- /// Feature flag key
- /// Default value
- ///
- /// The .
- ///
- public abstract Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
- EvaluationContext? context = null, CancellationToken cancellationToken = default);
+ ///
+ /// Resolves a boolean feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ ///
+ /// The .
+ ///
+ public abstract Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default);
- ///
- /// Resolves a string feature flag
- ///
- /// Feature flag key
- /// Default value
- ///
- /// The .
- ///
- public abstract Task> ResolveStringValueAsync(string flagKey, string defaultValue,
- EvaluationContext? context = null, CancellationToken cancellationToken = default);
+ ///
+ /// Resolves a string feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ ///
+ /// The .
+ ///
+ public abstract Task> ResolveStringValueAsync(string flagKey, string defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default);
- ///
- /// Resolves a integer feature flag
- ///
- /// Feature flag key
- /// Default value
- ///
- /// The .
- ///
- public abstract Task> ResolveIntegerValueAsync(string flagKey, int defaultValue,
- EvaluationContext? context = null, CancellationToken cancellationToken = default);
+ ///
+ /// Resolves a integer feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ ///
+ /// The .
+ ///
+ public abstract Task> ResolveIntegerValueAsync(string flagKey, int defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default);
- ///
- /// Resolves a double feature flag
- ///
- /// Feature flag key
- /// Default value
- ///
- /// The .
- ///
- public abstract Task> ResolveDoubleValueAsync(string flagKey, double defaultValue,
- EvaluationContext? context = null, CancellationToken cancellationToken = default);
+ ///
+ /// Resolves a double feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ ///
+ /// The .
+ ///
+ public abstract Task> ResolveDoubleValueAsync(string flagKey, double defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default);
- ///
- /// Resolves a structured feature flag
- ///
- /// Feature flag key
- /// Default value
- ///
- /// The .
- ///
- public abstract Task> ResolveStructureValueAsync(string flagKey, Value defaultValue,
- EvaluationContext? context = null, CancellationToken cancellationToken = default);
+ ///
+ /// Resolves a structured feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ ///
+ /// The .
+ ///
+ public abstract Task> ResolveStructureValueAsync(string flagKey, Value defaultValue,
+ EvaluationContext? context = null, CancellationToken cancellationToken = default);
- ///
- /// Internally-managed provider status.
- /// The SDK uses this field to track the status of the provider.
- /// Not visible outside OpenFeature assembly
- ///
- internal virtual ProviderStatus Status { get; set; } = ProviderStatus.NotReady;
+ ///
+ /// Internally-managed provider status.
+ /// The SDK uses this field to track the status of the provider.
+ /// Not visible outside OpenFeature assembly
+ ///
+ internal virtual ProviderStatus Status { get; set; } = ProviderStatus.NotReady;
- ///
- ///
- /// This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
- /// if they have special initialization needed prior being called for flag evaluation.
- /// When this method completes, the provider will be considered ready for use.
- ///
- ///
- ///
- /// The to cancel any async side effects.
- /// A task that completes when the initialization process is complete.
- ///
- ///
- /// Providers not implementing this method will be considered ready immediately.
- ///
- ///
- public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)
- {
- // Intentionally left blank.
- return Task.CompletedTask;
- }
+ ///
+ ///
+ /// This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
+ /// if they have special initialization needed prior being called for flag evaluation.
+ /// When this method completes, the provider will be considered ready for use.
+ ///
+ ///
+ ///
+ /// The to cancel any async side effects.
+ /// A task that completes when the initialization process is complete.
+ ///
+ ///
+ /// Providers not implementing this method will be considered ready immediately.
+ ///
+ ///
+ public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)
+ {
+ // Intentionally left blank.
+ return Task.CompletedTask;
+ }
- ///
- /// This method is called when a new provider is about to be used to evaluate flags, or the SDK is shut down.
- /// Providers can overwrite this method, if they have special shutdown actions needed.
- ///
- /// A task that completes when the shutdown process is complete.
- /// The to cancel any async side effects.
- public virtual Task ShutdownAsync(CancellationToken cancellationToken = default)
- {
- // Intentionally left blank.
- return Task.CompletedTask;
- }
+ ///
+ /// This method is called when a new provider is about to be used to evaluate flags, or the SDK is shut down.
+ /// Providers can overwrite this method, if they have special shutdown actions needed.
+ ///
+ /// A task that completes when the shutdown process is complete.
+ /// The to cancel any async side effects.
+ public virtual Task ShutdownAsync(CancellationToken cancellationToken = default)
+ {
+ // Intentionally left blank.
+ return Task.CompletedTask;
+ }
- ///
- /// Returns the event channel of the provider.
- ///
- /// The event channel of the provider
- public Channel GetEventChannel() => this.EventChannel;
+ ///
+ /// Returns the event channel of the provider.
+ ///
+ /// The event channel of the provider
+ public Channel GetEventChannel() => this.EventChannel;
- ///
- /// Track a user action or application state, usually representing a business objective or outcome. The implementation of this method is optional.
- ///
- /// The name associated with this tracking event
- /// The evaluation context used in the evaluation of the flag (optional)
- /// Data pertinent to the tracking event (Optional)
- public virtual void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default)
- {
- // Intentionally left blank.
- }
+ ///
+ /// Track a user action or application state, usually representing a business objective or outcome. The implementation of this method is optional.
+ ///
+ /// The name associated with this tracking event
+ /// The evaluation context used in the evaluation of the flag (optional)
+ /// Data pertinent to the tracking event (Optional)
+ public virtual void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default)
+ {
+ // Intentionally left blank.
}
}
diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs
index c1dbbe382..d38550ffd 100644
--- a/src/OpenFeature/Hook.cs
+++ b/src/OpenFeature/Hook.cs
@@ -4,85 +4,84 @@
using System.Threading.Tasks;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+///
+/// The Hook abstract class describes the default implementation for a hook.
+/// A hook has multiple lifecycles, and is called in the following order when normal execution Before, After, Finally.
+/// When an abnormal execution occurs, the hook is called in the following order: Error, Finally.
+///
+/// Before: immediately before flag evaluation
+/// After: immediately after successful flag evaluation
+/// Error: immediately after an unsuccessful during flag evaluation
+/// Finally: unconditionally after flag evaluation
+///
+/// Hooks can be configured to run globally (impacting all flag evaluations), per client, or per flag evaluation invocation.
+///
+///
+/// Hook Specification
+public abstract class Hook
{
///
- /// The Hook abstract class describes the default implementation for a hook.
- /// A hook has multiple lifecycles, and is called in the following order when normal execution Before, After, Finally.
- /// When an abnormal execution occurs, the hook is called in the following order: Error, Finally.
- ///
- /// Before: immediately before flag evaluation
- /// After: immediately after successful flag evaluation
- /// Error: immediately after an unsuccessful during flag evaluation
- /// Finally: unconditionally after flag evaluation
- ///
- /// Hooks can be configured to run globally (impacting all flag evaluations), per client, or per flag evaluation invocation.
- ///
+ /// Called immediately before flag evaluation.
///
- /// Hook Specification
- public abstract class Hook
+ /// Provides context of innovation
+ /// Caller provided data
+ /// The .
+ /// 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)
{
- ///
- /// Called immediately before flag evaluation.
- ///
- /// Provides context of innovation
- /// Caller provided data
- /// The .
- /// 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)
- {
- return new ValueTask(EvaluationContext.Empty);
- }
+ return new ValueTask(EvaluationContext.Empty);
+ }
- ///
- /// Called immediately after successful flag evaluation.
- ///
- /// Provides context of innovation
- /// Flag evaluation information
- /// 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)
- {
- return new ValueTask();
- }
+ ///
+ /// Called immediately after successful flag evaluation.
+ ///
+ /// Provides context of innovation
+ /// Flag evaluation information
+ /// 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)
+ {
+ return new ValueTask();
+ }
- ///
- /// Called immediately after an unsuccessful flag evaluation.
- ///
- /// Provides context of innovation
- /// Exception representing what went wrong
- /// 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)
- {
- return new ValueTask();
- }
+ ///
+ /// Called immediately after an unsuccessful flag evaluation.
+ ///
+ /// Provides context of innovation
+ /// Exception representing what went wrong
+ /// 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)
+ {
+ return new ValueTask();
+ }
- ///
- /// 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,
- FlagEvaluationDetails evaluationDetails,
- IReadOnlyDictionary? hints = null,
- CancellationToken cancellationToken = default)
- {
- return new ValueTask();
- }
+ ///
+ /// 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,
+ FlagEvaluationDetails evaluationDetails,
+ IReadOnlyDictionary? hints = null,
+ CancellationToken cancellationToken = default)
+ {
+ return new ValueTask();
}
}
diff --git a/src/OpenFeature/HookData.cs b/src/OpenFeature/HookData.cs
index 5d56eb870..ecfdfabd4 100644
--- a/src/OpenFeature/HookData.cs
+++ b/src/OpenFeature/HookData.cs
@@ -2,103 +2,102 @@
using System.Collections.Immutable;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+///
+/// A key-value collection of strings to objects used for passing data between hook stages.
+///
+/// This collection is scoped to a single evaluation for a single hook. Each hook stage for the evaluation
+/// will share the same .
+///
+///
+/// This collection is intended for use only during the execution of individual hook stages, a reference
+/// to the collection should not be retained.
+///
+///
+/// This collection is not thread-safe.
+///
+///
+///
+public sealed class HookData
{
+ private readonly Dictionary _data = [];
+
///
- /// A key-value collection of strings to objects used for passing data between hook stages.
- ///
- /// This collection is scoped to a single evaluation for a single hook. Each hook stage for the evaluation
- /// will share the same .
- ///
- ///
- /// This collection is intended for use only during the execution of individual hook stages, a reference
- /// to the collection should not be retained.
- ///
- ///
- /// This collection is not thread-safe.
- ///
+ /// Set the key to the given value.
///
- ///
- public sealed class HookData
+ /// The key for the value
+ /// The value to set
+ /// This hook data instance
+ public HookData Set(string key, object value)
{
- private readonly Dictionary _data = [];
-
- ///
- /// Set the key to the given value.
- ///
- /// The key for the value
- /// The value to set
- /// This hook data instance
- public HookData Set(string key, object value)
- {
- this._data[key] = value;
- return this;
- }
+ this._data[key] = value;
+ return this;
+ }
- ///
- /// Gets the value at the specified key as an object.
- ///
- /// For types use instead.
- ///
- ///
- /// The key of the value to be retrieved
- /// The object associated with the key
- ///
- /// Thrown when the context does not contain the specified key
- ///
- public object Get(string key)
- {
- return this._data[key];
- }
+ ///
+ /// Gets the value at the specified key as an object.
+ ///
+ /// For types use instead.
+ ///
+ ///
+ /// The key of the value to be retrieved
+ /// The object associated with the key
+ ///
+ /// Thrown when the context does not contain the specified key
+ ///
+ public object Get(string key)
+ {
+ return this._data[key];
+ }
- ///
- /// Return a count of all values.
- ///
- public int Count => this._data.Count;
+ ///
+ /// Return a count of all values.
+ ///
+ public int Count => this._data.Count;
- ///
- /// Return an enumerator for all values.
- ///
- /// An enumerator for all values
- public IEnumerator> GetEnumerator()
- {
- return this._data.GetEnumerator();
- }
+ ///
+ /// Return an enumerator for all values.
+ ///
+ /// An enumerator for all values
+ public IEnumerator> GetEnumerator()
+ {
+ return this._data.GetEnumerator();
+ }
- ///
- /// Return a list containing all the keys in the hook data
- ///
- public IImmutableList Keys => this._data.Keys.ToImmutableList();
+ ///
+ /// Return a list containing all the keys in the hook data
+ ///
+ public IImmutableList Keys => this._data.Keys.ToImmutableList();
- ///
- /// Return an enumerable containing all the values of the hook data
- ///
- public IImmutableList Values => this._data.Values.ToImmutableList();
+ ///
+ /// Return an enumerable containing all the values of the hook data
+ ///
+ public IImmutableList Values => this._data.Values.ToImmutableList();
- ///
- /// Gets all values as a read only dictionary.
- ///
- /// The dictionary references the original values and is not a thread-safe copy.
- ///
- ///
- /// A representation of the hook data
- public IReadOnlyDictionary AsDictionary()
- {
- return this._data;
- }
+ ///
+ /// Gets all values as a read only dictionary.
+ ///
+ /// The dictionary references the original values and is not a thread-safe copy.
+ ///
+ ///
+ /// A representation of the hook data
+ public IReadOnlyDictionary AsDictionary()
+ {
+ return this._data;
+ }
- ///
- /// Gets or sets the value associated with the specified key.
- ///
- /// The key of the value to get or set
- /// The value associated with the specified key
- ///
- /// Thrown when getting a value and the context does not contain the specified key
- ///
- public object this[string key]
- {
- get => this.Get(key);
- set => this.Set(key, value);
- }
+ ///
+ /// Gets or sets the value associated with the specified key.
+ ///
+ /// The key of the value to get or set
+ /// The value associated with the specified key
+ ///
+ /// Thrown when getting a value and the context does not contain the specified key
+ ///
+ public object this[string key]
+ {
+ get => this.Get(key);
+ set => this.Set(key, value);
}
}
diff --git a/src/OpenFeature/HookRunner.cs b/src/OpenFeature/HookRunner.cs
index 8c1dbb510..c80b86131 100644
--- a/src/OpenFeature/HookRunner.cs
+++ b/src/OpenFeature/HookRunner.cs
@@ -6,168 +6,167 @@
using Microsoft.Extensions.Logging;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+///
+/// This class manages the execution of hooks.
+///
+/// type of the evaluation detail provided to the hooks
+internal partial class HookRunner
{
+ private readonly ImmutableList _hooks;
+
+ private readonly List> _hookContexts;
+
+ private EvaluationContext _evaluationContext;
+
+ private readonly ILogger _logger;
+
///
- /// This class manages the execution of hooks.
+ /// Construct a hook runner instance. Each instance should be used for the execution of a single evaluation.
///
- /// type of the evaluation detail provided to the hooks
- internal partial class HookRunner
+ ///
+ /// The hooks for the evaluation, these should be in the correct order for the before evaluation stage
+ ///
+ ///
+ /// The initial evaluation context, this can be updated as the hooks execute
+ ///
+ ///
+ /// Contents of the initial hook context excluding the evaluation context and hook data
+ ///
+ /// Client logger instance
+ public HookRunner(ImmutableList hooks, EvaluationContext evaluationContext,
+ SharedHookContext sharedHookContext,
+ ILogger logger)
{
- private readonly ImmutableList _hooks;
-
- private readonly List> _hookContexts;
-
- private EvaluationContext _evaluationContext;
-
- private readonly ILogger _logger;
-
- ///
- /// Construct a hook runner instance. Each instance should be used for the execution of a single evaluation.
- ///
- ///
- /// The hooks for the evaluation, these should be in the correct order for the before evaluation stage
- ///
- ///
- /// The initial evaluation context, this can be updated as the hooks execute
- ///
- ///
- /// Contents of the initial hook context excluding the evaluation context and hook data
- ///
- /// Client logger instance
- public HookRunner(ImmutableList hooks, EvaluationContext evaluationContext,
- SharedHookContext sharedHookContext,
- ILogger logger)
+ this._evaluationContext = evaluationContext;
+ this._logger = logger;
+ this._hooks = hooks;
+ this._hookContexts = new List>(hooks.Count);
+ for (var i = 0; i < hooks.Count; i++)
{
- this._evaluationContext = evaluationContext;
- this._logger = logger;
- this._hooks = hooks;
- this._hookContexts = new List>(hooks.Count);
- for (var i = 0; i < hooks.Count; i++)
- {
- // Create hook instance specific hook context.
- // Hook contexts are instance specific so that the mutable hook data is scoped to each hook.
- this._hookContexts.Add(sharedHookContext.ToHookContext(evaluationContext));
- }
+ // Create hook instance specific hook context.
+ // Hook contexts are instance specific so that the mutable hook data is scoped to each hook.
+ this._hookContexts.Add(sharedHookContext.ToHookContext(evaluationContext));
}
+ }
+
+ ///
+ /// Execute before hooks.
+ ///
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ /// Context with any modifications from the before hooks
+ public async Task TriggerBeforeHooksAsync(IImmutableDictionary? hints,
+ CancellationToken cancellationToken = default)
+ {
+ var evalContextBuilder = EvaluationContext.Builder();
+ evalContextBuilder.Merge(this._evaluationContext);
- ///
- /// Execute before hooks.
- ///
- /// Optional hook hints
- /// Cancellation token which can cancel hook operations
- /// Context with any modifications from the before hooks
- public async Task TriggerBeforeHooksAsync(IImmutableDictionary? hints,
- CancellationToken cancellationToken = default)
+ for (var i = 0; i < this._hooks.Count; i++)
{
- var evalContextBuilder = EvaluationContext.Builder();
- evalContextBuilder.Merge(this._evaluationContext);
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
- for (var i = 0; i < this._hooks.Count; i++)
+ var resp = await hook.BeforeAsync(hookContext, hints, cancellationToken)
+ .ConfigureAwait(false);
+ if (resp != null)
{
- var hook = this._hooks[i];
- var hookContext = this._hookContexts[i];
-
- var resp = await hook.BeforeAsync(hookContext, hints, cancellationToken)
- .ConfigureAwait(false);
- if (resp != null)
- {
- evalContextBuilder.Merge(resp);
- this._evaluationContext = evalContextBuilder.Build();
- for (var j = 0; j < this._hookContexts.Count; j++)
- {
- this._hookContexts[j] = this._hookContexts[j].WithNewEvaluationContext(this._evaluationContext);
- }
- }
- else
+ evalContextBuilder.Merge(resp);
+ this._evaluationContext = evalContextBuilder.Build();
+ for (var j = 0; j < this._hookContexts.Count; j++)
{
- this.HookReturnedNull(hook.GetType().Name);
+ this._hookContexts[j] = this._hookContexts[j].WithNewEvaluationContext(this._evaluationContext);
}
}
+ else
+ {
+ this.HookReturnedNull(hook.GetType().Name);
+ }
+ }
- return this._evaluationContext;
+ return this._evaluationContext;
+ }
+
+ ///
+ /// Execute the after hooks. These are executed in opposite order of the before hooks.
+ ///
+ /// The evaluation details which will be provided to the hook
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ public async Task TriggerAfterHooksAsync(FlagEvaluationDetails evaluationDetails,
+ IImmutableDictionary? hints,
+ CancellationToken cancellationToken = default)
+ {
+ // After hooks run in reverse.
+ for (var i = this._hooks.Count - 1; i >= 0; i--)
+ {
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
+ await hook.AfterAsync(hookContext, evaluationDetails, hints, cancellationToken)
+ .ConfigureAwait(false);
}
+ }
- ///
- /// Execute the after hooks. These are executed in opposite order of the before hooks.
- ///
- /// The evaluation details which will be provided to the hook
- /// Optional hook hints
- /// Cancellation token which can cancel hook operations
- public async Task TriggerAfterHooksAsync(FlagEvaluationDetails evaluationDetails,
- IImmutableDictionary? hints,
- CancellationToken cancellationToken = default)
+ ///
+ /// Execute the error hooks. These are executed in opposite order of the before hooks.
+ ///
+ /// Exception which triggered the error
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ public async Task TriggerErrorHooksAsync(Exception exception,
+ IImmutableDictionary? hints, CancellationToken cancellationToken = default)
+ {
+ // Error hooks run in reverse.
+ for (var i = this._hooks.Count - 1; i >= 0; i--)
{
- // After hooks run in reverse.
- for (var i = this._hooks.Count - 1; i >= 0; i--)
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
+ try
{
- var hook = this._hooks[i];
- var hookContext = this._hookContexts[i];
- await hook.AfterAsync(hookContext, evaluationDetails, hints, cancellationToken)
+ await hook.ErrorAsync(hookContext, exception, hints, cancellationToken)
.ConfigureAwait(false);
}
- }
-
- ///
- /// Execute the error hooks. These are executed in opposite order of the before hooks.
- ///
- /// Exception which triggered the error
- /// Optional hook hints
- /// Cancellation token which can cancel hook operations
- public async Task TriggerErrorHooksAsync(Exception exception,
- IImmutableDictionary? hints, CancellationToken cancellationToken = default)
- {
- // Error hooks run in reverse.
- for (var i = this._hooks.Count - 1; i >= 0; i--)
+ catch (Exception e)
{
- var hook = this._hooks[i];
- var hookContext = this._hookContexts[i];
- try
- {
- await hook.ErrorAsync(hookContext, exception, hints, cancellationToken)
- .ConfigureAwait(false);
- }
- catch (Exception e)
- {
- this.ErrorHookError(hook.GetType().Name, e);
- }
+ this.ErrorHookError(hook.GetType().Name, e);
}
}
+ }
- ///
- /// Execute the finally hooks. These are executed in opposite order of the before hooks.
- ///
- /// The evaluation details which will be provided to the hook
- /// Optional hook hints
- /// Cancellation token which can cancel hook operations
- public async Task TriggerFinallyHooksAsync(FlagEvaluationDetails evaluationDetails,
- IImmutableDictionary? hints,
- CancellationToken cancellationToken = default)
+ ///
+ /// Execute the finally hooks. These are executed in opposite order of the before hooks.
+ ///
+ /// The evaluation details which will be provided to the hook
+ /// Optional hook hints
+ /// Cancellation token which can cancel hook operations
+ public async Task TriggerFinallyHooksAsync(FlagEvaluationDetails evaluationDetails,
+ IImmutableDictionary? hints,
+ CancellationToken cancellationToken = default)
+ {
+ // Finally hooks run in reverse
+ for (var i = this._hooks.Count - 1; i >= 0; i--)
{
- // Finally hooks run in reverse
- for (var i = this._hooks.Count - 1; i >= 0; i--)
+ var hook = this._hooks[i];
+ var hookContext = this._hookContexts[i];
+ try
{
- var hook = this._hooks[i];
- var hookContext = this._hookContexts[i];
- try
- {
- await hook.FinallyAsync(hookContext, evaluationDetails, hints, cancellationToken)
- .ConfigureAwait(false);
- }
- catch (Exception e)
- {
- this.FinallyHookError(hook.GetType().Name, e);
- }
+ await hook.FinallyAsync(hookContext, evaluationDetails, hints, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ this.FinallyHookError(hook.GetType().Name, e);
}
}
+ }
- [LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")]
- partial void HookReturnedNull(string hookName);
+ [LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")]
+ partial void HookReturnedNull(string hookName);
- [LoggerMessage(103, LogLevel.Error, "Error while executing Error hook {HookName}")]
- partial void ErrorHookError(string hookName, Exception exception);
+ [LoggerMessage(103, LogLevel.Error, "Error while executing Error hook {HookName}")]
+ partial void ErrorHookError(string hookName, Exception exception);
- [LoggerMessage(104, LogLevel.Error, "Error while executing Finally hook {HookName}")]
- partial void FinallyHookError(string hookName, Exception exception);
- }
+ [LoggerMessage(104, LogLevel.Error, "Error while executing Finally hook {HookName}")]
+ partial void FinallyHookError(string hookName, Exception exception);
}
diff --git a/src/OpenFeature/Hooks/LoggingHook.cs b/src/OpenFeature/Hooks/LoggingHook.cs
index a8d318b00..b83081678 100644
--- a/src/OpenFeature/Hooks/LoggingHook.cs
+++ b/src/OpenFeature/Hooks/LoggingHook.cs
@@ -6,169 +6,168 @@
using Microsoft.Extensions.Logging;
using OpenFeature.Model;
-namespace OpenFeature.Hooks
+namespace OpenFeature.Hooks;
+
+///
+/// The logging hook is a hook which logs messages during the flag evaluation life-cycle.
+///
+public sealed partial class LoggingHook : Hook
{
+ private readonly ILogger _logger;
+ private readonly bool _includeContext;
+
///
- /// The logging hook is a hook which logs messages during the flag evaluation life-cycle.
+ /// Initialise a with a and optional Evaluation Context. will
+ /// include properties in the to the generated logs.
///
- public sealed partial class LoggingHook : Hook
+ public LoggingHook(ILogger logger, bool includeContext = false)
{
- private readonly ILogger _logger;
- private readonly bool _includeContext;
-
- ///
- /// Initialise a with a and optional Evaluation Context. will
- /// include properties in the to the generated logs.
- ///
- public LoggingHook(ILogger logger, bool includeContext = false)
- {
- this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
- this._includeContext = includeContext;
- }
+ this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ this._includeContext = includeContext;
+ }
- ///
- public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
- {
- var evaluationContext = this._includeContext ? context.EvaluationContext : null;
+ ///
+ public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ var evaluationContext = this._includeContext ? context.EvaluationContext : null;
- var content = new LoggingHookContent(
- context.ClientMetadata.Name,
- context.ProviderMetadata.Name,
- context.FlagKey,
- context.DefaultValue?.ToString(),
- evaluationContext);
+ var content = new LoggingHookContent(
+ context.ClientMetadata.Name,
+ context.ProviderMetadata.Name,
+ context.FlagKey,
+ context.DefaultValue?.ToString(),
+ evaluationContext);
- this.HookBeforeStageExecuted(content);
+ this.HookBeforeStageExecuted(content);
- return base.BeforeAsync(context, hints, cancellationToken);
- }
+ return base.BeforeAsync(context, hints, cancellationToken);
+ }
- ///
- public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
- {
- var evaluationContext = this._includeContext ? context.EvaluationContext : null;
+ ///
+ public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ var evaluationContext = this._includeContext ? context.EvaluationContext : null;
- var content = new LoggingHookContent(
- context.ClientMetadata.Name,
- context.ProviderMetadata.Name,
- context.FlagKey,
- context.DefaultValue?.ToString(),
- evaluationContext);
+ var content = new LoggingHookContent(
+ context.ClientMetadata.Name,
+ context.ProviderMetadata.Name,
+ context.FlagKey,
+ context.DefaultValue?.ToString(),
+ evaluationContext);
- this.HookErrorStageExecuted(content);
+ this.HookErrorStageExecuted(content);
- return base.ErrorAsync(context, error, hints, cancellationToken);
- }
+ return base.ErrorAsync(context, error, hints, cancellationToken);
+ }
- ///
- public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
- {
- var evaluationContext = this._includeContext ? context.EvaluationContext : null;
+ ///
+ public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ var evaluationContext = this._includeContext ? context.EvaluationContext : null;
- var content = new LoggingHookContent(
- context.ClientMetadata.Name,
- context.ProviderMetadata.Name,
- context.FlagKey,
- context.DefaultValue?.ToString(),
- evaluationContext);
+ var content = new LoggingHookContent(
+ context.ClientMetadata.Name,
+ context.ProviderMetadata.Name,
+ context.FlagKey,
+ context.DefaultValue?.ToString(),
+ evaluationContext);
- this.HookAfterStageExecuted(content);
+ this.HookAfterStageExecuted(content);
- return base.AfterAsync(context, details, hints, cancellationToken);
- }
+ return base.AfterAsync(context, details, hints, cancellationToken);
+ }
- [LoggerMessage(
- Level = LogLevel.Debug,
- Message = "Before Flag Evaluation {Content}")]
- partial void HookBeforeStageExecuted(LoggingHookContent content);
-
- [LoggerMessage(
- Level = LogLevel.Error,
- Message = "Error during Flag Evaluation {Content}")]
- partial void HookErrorStageExecuted(LoggingHookContent content);
-
- [LoggerMessage(
- Level = LogLevel.Debug,
- Message = "After Flag Evaluation {Content}")]
- partial void HookAfterStageExecuted(LoggingHookContent content);
-
- ///
- /// Generates a log string with contents provided by the .
- ///
- /// Specification for log contents found at https://github.com/open-feature/spec/blob/d261f68331b94fd8ed10bc72bc0485cfc72a51a8/specification/appendix-a-included-utilities.md#logging-hook
- ///
- ///
- internal class LoggingHookContent
- {
- private readonly string _domain;
- private readonly string _providerName;
- private readonly string _flagKey;
- private readonly string _defaultValue;
- private readonly EvaluationContext? _evaluationContext;
+ [LoggerMessage(
+ Level = LogLevel.Debug,
+ Message = "Before Flag Evaluation {Content}")]
+ partial void HookBeforeStageExecuted(LoggingHookContent content);
- public LoggingHookContent(string? domain, string? providerName, string flagKey, string? defaultValue, EvaluationContext? evaluationContext = null)
- {
- this._domain = string.IsNullOrEmpty(domain) ? "missing" : domain!;
- this._providerName = string.IsNullOrEmpty(providerName) ? "missing" : providerName!;
- this._flagKey = flagKey;
- this._defaultValue = string.IsNullOrEmpty(defaultValue) ? "missing" : defaultValue!;
- this._evaluationContext = evaluationContext;
- }
+ [LoggerMessage(
+ Level = LogLevel.Error,
+ Message = "Error during Flag Evaluation {Content}")]
+ partial void HookErrorStageExecuted(LoggingHookContent content);
- public override string ToString()
- {
- var stringBuilder = new StringBuilder();
+ [LoggerMessage(
+ Level = LogLevel.Debug,
+ Message = "After Flag Evaluation {Content}")]
+ partial void HookAfterStageExecuted(LoggingHookContent content);
- stringBuilder.Append("Domain:");
- stringBuilder.AppendLine(this._domain);
+ ///
+ /// Generates a log string with contents provided by the .
+ ///
+ /// Specification for log contents found at https://github.com/open-feature/spec/blob/d261f68331b94fd8ed10bc72bc0485cfc72a51a8/specification/appendix-a-included-utilities.md#logging-hook
+ ///
+ ///
+ internal class LoggingHookContent
+ {
+ private readonly string _domain;
+ private readonly string _providerName;
+ private readonly string _flagKey;
+ private readonly string _defaultValue;
+ private readonly EvaluationContext? _evaluationContext;
+
+ public LoggingHookContent(string? domain, string? providerName, string flagKey, string? defaultValue, EvaluationContext? evaluationContext = null)
+ {
+ this._domain = string.IsNullOrEmpty(domain) ? "missing" : domain!;
+ this._providerName = string.IsNullOrEmpty(providerName) ? "missing" : providerName!;
+ this._flagKey = flagKey;
+ this._defaultValue = string.IsNullOrEmpty(defaultValue) ? "missing" : defaultValue!;
+ this._evaluationContext = evaluationContext;
+ }
- stringBuilder.Append("ProviderName:");
- stringBuilder.AppendLine(this._providerName);
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
- stringBuilder.Append("FlagKey:");
- stringBuilder.AppendLine(this._flagKey);
+ stringBuilder.Append("Domain:");
+ stringBuilder.AppendLine(this._domain);
- stringBuilder.Append("DefaultValue:");
- stringBuilder.AppendLine(this._defaultValue);
+ stringBuilder.Append("ProviderName:");
+ stringBuilder.AppendLine(this._providerName);
- if (this._evaluationContext != null)
- {
- stringBuilder.AppendLine("Context:");
- foreach (var kvp in this._evaluationContext.AsDictionary())
- {
- stringBuilder.Append('\t');
- stringBuilder.Append(kvp.Key);
- stringBuilder.Append(':');
- stringBuilder.AppendLine(GetValueString(kvp.Value));
- }
- }
+ stringBuilder.Append("FlagKey:");
+ stringBuilder.AppendLine(this._flagKey);
- return stringBuilder.ToString();
- }
+ stringBuilder.Append("DefaultValue:");
+ stringBuilder.AppendLine(this._defaultValue);
- static string? GetValueString(Value value)
+ if (this._evaluationContext != null)
{
- if (value.IsNull)
- return string.Empty;
+ stringBuilder.AppendLine("Context:");
+ foreach (var kvp in this._evaluationContext.AsDictionary())
+ {
+ stringBuilder.Append('\t');
+ stringBuilder.Append(kvp.Key);
+ stringBuilder.Append(':');
+ stringBuilder.AppendLine(GetValueString(kvp.Value));
+ }
+ }
- if (value.IsString)
- return value.AsString;
+ return stringBuilder.ToString();
+ }
- if (value.IsBoolean)
- return value.AsBoolean.ToString();
+ static string? GetValueString(Value value)
+ {
+ if (value.IsNull)
+ return string.Empty;
- if (value.IsNumber)
- {
- // Value.AsDouble will attempt to cast other numbers to double
- // There is an implicit conversation for int/long to double
- if (value.AsDouble != null) return value.AsDouble.ToString();
- }
+ if (value.IsString)
+ return value.AsString;
- if (value.IsDateTime)
- return value.AsDateTime?.ToString("O");
+ if (value.IsBoolean)
+ return value.AsBoolean.ToString();
- return value.ToString();
+ if (value.IsNumber)
+ {
+ // Value.AsDouble will attempt to cast other numbers to double
+ // There is an implicit conversation for int/long to double
+ if (value.AsDouble != null) return value.AsDouble.ToString();
}
+
+ if (value.IsDateTime)
+ return value.AsDateTime?.ToString("O");
+
+ return value.ToString();
}
}
}
diff --git a/src/OpenFeature/IEventBus.cs b/src/OpenFeature/IEventBus.cs
index 114b66b3d..bb1cd91e2 100644
--- a/src/OpenFeature/IEventBus.cs
+++ b/src/OpenFeature/IEventBus.cs
@@ -1,24 +1,23 @@
using OpenFeature.Constant;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+///
+/// Defines the methods required for handling events.
+///
+public interface IEventBus
{
///
- /// Defines the methods required for handling events.
+ /// Adds an Event Handler for the given event type.
+ ///
+ /// The type of the event
+ /// Implementation of the
+ void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler);
+ ///
+ /// Removes an Event Handler for the given event type.
///
- public interface IEventBus
- {
- ///
- /// Adds an Event Handler for the given event type.
- ///
- /// The type of the event
- /// Implementation of the
- void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler);
- ///
- /// Removes an Event Handler for the given event type.
- ///
- /// The type of the event
- /// Implementation of the
- void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler);
- }
+ /// The type of the event
+ /// Implementation of the
+ void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler);
}
diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs
index f39b7f527..c14e6e4bf 100644
--- a/src/OpenFeature/IFeatureClient.cs
+++ b/src/OpenFeature/IFeatureClient.cs
@@ -4,170 +4,169 @@
using OpenFeature.Constant;
using OpenFeature.Model;
-namespace OpenFeature
+namespace OpenFeature;
+
+///
+/// Interface used to resolve flags of varying types.
+///
+public interface IFeatureClient : IEventBus
{
///
- /// Interface used to resolve flags of varying types.
- ///
- public interface IFeatureClient : IEventBus
- {
- ///
- /// Appends hooks to client
- ///
- /// The appending operation will be atomic.
- ///
- ///
- /// A list of Hooks that implement the interface
- void AddHooks(IEnumerable hooks);
-
- ///
- /// Enumerates the global hooks.
- ///
- /// The items enumerated will reflect the registered hooks
- /// at the start of enumeration. Hooks added during enumeration
- /// will not be included.
- ///
- ///
- /// Enumeration of
- IEnumerable GetHooks();
-
- ///
- /// Gets the of this client
- ///
- /// The evaluation context may be set from multiple threads, when accessing the client evaluation context
- /// it should be accessed once for an operation, and then that reference should be used for all dependent
- /// operations.
- ///
- ///
- /// of this client
- EvaluationContext GetContext();
-
- ///
- /// Sets the of the client
- ///
- /// The to set
- void SetContext(EvaluationContext context);
-
- ///
- /// Gets client metadata
- ///
- /// Client metadata
- ClientMetadata GetMetadata();
-
- ///
- /// Returns the current status of the associated provider.
- ///
- ///
- ProviderStatus ProviderStatus { get; }
+ /// Appends hooks to client
+ ///
+ /// The appending operation will be atomic.
+ ///
+ ///
+ /// A list of Hooks that implement the interface
+ void AddHooks(IEnumerable hooks);
+
+ ///
+ /// Enumerates the global hooks.
+ ///
+ /// The items enumerated will reflect the registered hooks
+ /// at the start of enumeration. Hooks added during enumeration
+ /// will not be included.
+ ///
+ ///
+ /// Enumeration of
+ IEnumerable GetHooks();
+
+ ///
+ /// Gets the of this client
+ ///
+ /// The evaluation context may be set from multiple threads, when accessing the client evaluation context
+ /// it should be accessed once for an operation, and then that reference should be used for all dependent
+ /// operations.
+ ///
+ ///
+ /// of this client
+ EvaluationContext GetContext();
+
+ ///
+ /// Sets the of the client
+ ///
+ /// The to set
+ void SetContext(EvaluationContext context);
+
+ ///
+ /// Gets client metadata
+ ///
+ /// Client metadata
+ ClientMetadata GetMetadata();
+
+ ///
+ /// Returns the current status of the associated provider.
+ ///
+ ///
+ ProviderStatus ProviderStatus { get; }
- ///
- /// Resolves a boolean feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag value.
- Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a boolean feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag details
- Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a string feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag value.
- Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a string feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag details
- Task> GetStringDetailsAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a integer feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag value.
- Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a integer feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag details
- Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a double feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag value.
- Task GetDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a double feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag details
- Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a structure object feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag value.
- Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
-
- ///
- /// Resolves a structure object feature flag
- ///
- /// Feature flag key
- /// Default value
- /// Evaluation Context
- /// Flag Evaluation Options
- /// The .
- /// Resolved flag details
- Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
- }
+ ///
+ /// Resolves a boolean feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag value.
+ Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a boolean feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag details
+ Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a string feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag value.
+ Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a string feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag details
+ Task> GetStringDetailsAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a integer feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag value.
+ Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a integer feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag details
+ Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a double feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag value.
+ Task GetDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a double feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag details
+ Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a structure object feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag value.
+ Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves a structure object feature flag
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Evaluation Context
+ /// Flag Evaluation Options
+ /// The .
+ /// Resolved flag details
+ Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default);
}
diff --git a/src/OpenFeature/Model/ClientMetadata.cs b/src/OpenFeature/Model/ClientMetadata.cs
index b98e6e9da..ffdc4eebe 100644
--- a/src/OpenFeature/Model/ClientMetadata.cs
+++ b/src/OpenFeature/Model/ClientMetadata.cs
@@ -1,23 +1,22 @@
-namespace OpenFeature.Model
+namespace OpenFeature.Model;
+
+///
+/// Represents the client metadata
+///
+public sealed class ClientMetadata : Metadata
{
///
- /// Represents the client metadata
+ /// Version of the client
///
- public sealed class ClientMetadata : Metadata
- {
- ///
- /// Version of the client
- ///
- public string? Version { get; }
+ public string? Version { get; }
- ///
- /// Initializes a new instance of the class
- ///
- /// Name of client
- /// Version of client
- public ClientMetadata(string? name, string? version) : base(name)
- {
- this.Version = version;
- }
+ ///
+ /// Initializes a new instance of the class
+ ///
+ /// Name of client
+ /// Version of client
+ public ClientMetadata(string? name, string? version) : base(name)
+ {
+ this.Version = version;
}
}
diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs
index 304e4cd9d..ed4f989a8 100644
--- a/src/OpenFeature/Model/EvaluationContext.cs
+++ b/src/OpenFeature/Model/EvaluationContext.cs
@@ -2,122 +2,121 @@
using System.Collections.Generic;
using System.Collections.Immutable;
-namespace OpenFeature.Model
+namespace OpenFeature.Model;
+
+///
+/// A KeyValuePair with a string key and object value that is used to apply user defined properties
+/// to the feature flag evaluation context.
+///
+/// Evaluation context
+public sealed class EvaluationContext
{
///
- /// A KeyValuePair with a string key and object value that is used to apply user defined properties
- /// to the feature flag evaluation context.
+ /// The index for the "targeting key" property when the EvaluationContext is serialized or expressed as a dictionary.
+ ///
+ internal const string TargetingKeyIndex = "targetingKey";
+
+
+ private readonly Structure _structure;
+
+ ///
+ /// Internal constructor used by the builder.
///
- /// Evaluation context
- public sealed class EvaluationContext
+ ///
+ internal EvaluationContext(Structure content)
{
- ///
- /// The index for the "targeting key" property when the EvaluationContext is serialized or expressed as a dictionary.
- ///
- internal const string TargetingKeyIndex = "targetingKey";
+ this._structure = content;
+ }
- private readonly Structure _structure;
+ ///
+ /// Private constructor for making an empty .
+ ///
+ private EvaluationContext()
+ {
+ this._structure = Structure.Empty;
+ }
- ///
- /// Internal constructor used by the builder.
- ///
- ///
- internal EvaluationContext(Structure content)
- {
- this._structure = content;
- }
+ ///
+ /// An empty evaluation context.
+ ///
+ public static EvaluationContext Empty { get; } = new EvaluationContext();
+ ///
+ /// Gets the Value at the specified key
+ ///
+ /// The key of the value to be retrieved
+ /// The associated with the key
+ ///
+ /// Thrown when the context does not contain the specified key
+ ///
+ ///
+ /// Thrown when the key is
+ ///
+ public Value GetValue(string key) => this._structure.GetValue(key);
- ///
- /// Private constructor for making an empty .
- ///
- private EvaluationContext()
- {
- this._structure = Structure.Empty;
- }
+ ///
+ /// Bool indicating if the specified key exists in the evaluation context
+ ///
+ /// The key of the value to be checked
+ /// indicating the presence of the key
+ ///
+ /// Thrown when the key is
+ ///
+ public bool ContainsKey(string key) => this._structure.ContainsKey(key);
- ///
- /// An empty evaluation context.
- ///
- public static EvaluationContext Empty { get; } = new EvaluationContext();
-
- ///
- /// Gets the Value at the specified key
- ///
- /// The key of the value to be retrieved
- /// The associated with the key
- ///
- /// Thrown when the context does not contain the specified key
- ///
- ///
- /// Thrown when the key is
- ///
- public Value GetValue(string key) => this._structure.GetValue(key);
-
- ///
- /// Bool indicating if the specified key exists in the evaluation context
- ///
- /// The key of the value to be checked
- /// indicating the presence of the key
- ///
- /// Thrown when the key is
- ///
- public bool ContainsKey(string key) => this._structure.ContainsKey(key);
-
- ///
- /// Gets the value associated with the specified key
- ///
- /// The or if the key was not present
- /// The key of the value to be retrieved
- /// indicating the presence of the key
- ///
- /// Thrown when the key is
- ///
- public bool TryGetValue(string key, out Value? value) => this._structure.TryGetValue(key, out value);
-
- ///
- /// Gets all values as a Dictionary
- ///
- /// New representation of this Structure
- public IImmutableDictionary AsDictionary()
- {
- return this._structure.AsDictionary();
- }
+ ///
+ /// Gets the value associated with the specified key
+ ///
+ /// The or if the key was not present
+ /// The key of the value to be retrieved
+ /// indicating the presence of the key
+ ///
+ /// Thrown when the key is
+ ///
+ public bool TryGetValue(string key, out Value? value) => this._structure.TryGetValue(key, out value);
- ///
- /// Return a count of all values
- ///
- public int Count => this._structure.Count;
+ ///
+ /// Gets all values as a Dictionary
+ ///
+ /// New representation of this Structure
+ public IImmutableDictionary AsDictionary()
+ {
+ return this._structure.AsDictionary();
+ }
- ///
- /// Returns the targeting key for the context.
- ///
- public string? TargetingKey
- {
- get
- {
- this._structure.TryGetValue(TargetingKeyIndex, out Value? targetingKey);
- return targetingKey?.AsString;
- }
- }
+ ///
+ /// Return a count of all values
+ ///
+ public int Count => this._structure.Count;
- ///
- /// Return an enumerator for all values
- ///
- /// An enumerator for all values
- public IEnumerator> GetEnumerator()
+ ///
+ /// Returns the targeting key for the context.
+ ///
+ public string? TargetingKey
+ {
+ get
{
- return this._structure.GetEnumerator();
+ this._structure.TryGetValue(TargetingKeyIndex, out Value? targetingKey);
+ return targetingKey?.AsString;
}
+ }
- ///
- /// Get a builder which can build an .
- ///
- /// The builder
- public static EvaluationContextBuilder Builder()
- {
- return new EvaluationContextBuilder();
- }
+ ///
+ /// Return an enumerator for all values
+ ///
+ /// An enumerator for all values
+ public IEnumerator> GetEnumerator()
+ {
+ return this._structure.GetEnumerator();
+ }
+
+ ///
+ /// Get a builder which can build an .
+ ///
+ /// The builder
+ public static EvaluationContextBuilder Builder()
+ {
+ return new EvaluationContextBuilder();
}
}
diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs
index 30e2ffe02..3d85ba984 100644
--- a/src/OpenFeature/Model/EvaluationContextBuilder.cs
+++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs
@@ -1,156 +1,155 @@
using System;
-namespace OpenFeature.Model
+namespace OpenFeature.Model;
+
+///
+/// A builder which allows the specification of attributes for an .
+///
+/// A object is intended for use by a single thread and should not be used
+/// from multiple threads. Once an has been created it is immutable and safe for use
+/// from multiple threads.
+///
+///
+public sealed class EvaluationContextBuilder
{
+ private readonly StructureBuilder _attributes = Structure.Builder();
+
///
- /// A builder which allows the specification of attributes for an .
- ///
- /// A object is intended for use by a single thread and should not be used
- /// from multiple threads. Once an has been created it is immutable and safe for use
- /// from multiple threads.
- ///
+ /// Internal to only allow direct creation by .
///
- public sealed class EvaluationContextBuilder
- {
- private readonly StructureBuilder _attributes = Structure.Builder();
+ internal EvaluationContextBuilder() { }
- ///
- /// Internal to only allow direct creation by .
- ///
- internal EvaluationContextBuilder() { }
+ ///
+ /// Set the targeting key for the context.
+ ///
+ /// The targeting key
+ /// This builder
+ public EvaluationContextBuilder SetTargetingKey(string targetingKey)
+ {
+ this._attributes.Set(EvaluationContext.TargetingKeyIndex, targetingKey);
+ return this;
+ }
- ///
- /// Set the targeting key for the context.
- ///
- /// The targeting key
- /// This builder
- public EvaluationContextBuilder SetTargetingKey(string targetingKey)
- {
- this._attributes.Set(EvaluationContext.TargetingKeyIndex, targetingKey);
- return this;
- }
+ ///
+ /// Set the key to the given .
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, Value value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given .
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, Value value)
- {
- this._attributes.Set(key, value);
- return this;
- }
+ ///
+ /// Set the key to the given string.
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, string value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given string.
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, string value)
- {
- this._attributes.Set(key, value);
- return this;
- }
+ ///
+ /// Set the key to the given int.
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, int value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given int.
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, int value)
- {
- this._attributes.Set(key, value);
- return this;
- }
+ ///
+ /// Set the key to the given double.
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, double value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given double.
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, double value)
- {
- this._attributes.Set(key, value);
- return this;
- }
+ ///
+ /// Set the key to the given long.
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, long value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given long.
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, long value)
- {
- this._attributes.Set(key, value);
- return this;
- }
+ ///
+ /// Set the key to the given bool.
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, bool value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given bool.
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, bool value)
- {
- this._attributes.Set(key, value);
- return this;
- }
+ ///
+ /// Set the key to the given .
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, Structure value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given .
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, Structure value)
- {
- this._attributes.Set(key, value);
- return this;
- }
+ ///
+ /// Set the key to the given DateTime.
+ ///
+ /// The key for the value
+ /// The value to set
+ /// This builder
+ public EvaluationContextBuilder Set(string key, DateTime value)
+ {
+ this._attributes.Set(key, value);
+ return this;
+ }
- ///
- /// Set the key to the given DateTime.
- ///
- /// The key for the value
- /// The value to set
- /// This builder
- public EvaluationContextBuilder Set(string key, DateTime value)
+ ///
+ /// Incorporate an existing context into the builder.
+ ///
+ /// Any existing keys in the builder will be replaced by keys in the context.
+ ///
+ ///
+ /// The context to add merge
+ /// This builder
+ public EvaluationContextBuilder Merge(EvaluationContext context)
+ {
+ foreach (var kvp in context)
{
- this._attributes.Set(key, value);
- return this;
+ this.Set(kvp.Key, kvp.Value);
}
- ///
- /// Incorporate an existing context into the builder.
- ///
- /// Any existing keys in the builder will be replaced by keys in the context.
- ///
- ///
- /// The context to add merge
- /// This builder
- public EvaluationContextBuilder Merge(EvaluationContext context)
- {
- foreach (var kvp in context)
- {
- this.Set(kvp.Key, kvp.Value);
- }
-
- return this;
- }
+ return this;
+ }
- ///
- /// Build an immutable .
- ///
- /// An immutable
- public EvaluationContext Build()
- {
- return new EvaluationContext(this._attributes.Build());
- }
+ ///
+ /// Build an immutable .
+ ///
+ /// An immutable
+ public EvaluationContext Build()
+ {
+ return new EvaluationContext(this._attributes.Build());
}
}
diff --git a/src/OpenFeature/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs
index 11283b4f6..a08e2041e 100644
--- a/src/OpenFeature/Model/FlagEvaluationDetails.cs
+++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs
@@ -1,75 +1,74 @@
using OpenFeature.Constant;
-namespace OpenFeature.Model
+namespace OpenFeature.Model;
+
+///
+/// The contract returned to the caller that describes the result of the flag evaluation process.
+///
+/// Flag value type
+///
+public sealed class FlagEvaluationDetails
{
///
- /// The contract returned to the caller that describes the result of the flag evaluation process.
+ /// Feature flag evaluated value
///
- /// Flag value type
- ///
- public sealed class FlagEvaluationDetails
- {
- ///
- /// Feature flag evaluated value
- ///
- public T Value { get; }
+ public T Value { get; }
- ///
- /// Feature flag key
- ///
- public string FlagKey { get; }
+ ///
+ /// Feature flag key
+ ///
+ public string FlagKey { get; }
- ///
- /// Error that occurred during evaluation
- ///
- public ErrorType ErrorType { get; }
+ ///
+ /// Error that occurred during evaluation
+ ///
+ public ErrorType ErrorType { get; }
- ///
- /// Message containing additional details about an error.
- ///
- /// Will be if there is no error or if the provider didn't provide any additional error
- /// details.
- ///
- ///
- public string? ErrorMessage { get; }
+ ///
+ /// Message containing additional details about an error.
+ ///
+ /// Will be if there is no error or if the provider didn't provide any additional error
+ /// details.
+ ///
+ ///
+ public string? ErrorMessage { get; }
- ///
- /// Describes the reason for the outcome of the evaluation process
- ///
- public string? Reason { get; }
+ ///
+ /// Describes the reason for the outcome of the evaluation process
+ ///
+ public string? Reason { get; }
- ///
- /// A variant is a semantic identifier for a value. This allows for referral to particular values without
- /// necessarily including the value itself, which may be quite prohibitively large or otherwise unsuitable
- /// in some cases.
- ///
- public string? Variant { get; }
+ ///
+ /// A variant is a semantic identifier for a value. This allows for referral to particular values without
+ /// necessarily including the value itself, which may be quite prohibitively large or otherwise unsuitable
+ /// in some cases.
+ ///
+ public string? Variant { get; }
- ///
- /// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, or number.
- ///
- public ImmutableMetadata? FlagMetadata { get; }
+ ///
+ /// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, or number.
+ ///
+ public ImmutableMetadata? FlagMetadata { get; }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Feature flag key
- /// Evaluated value
- /// Error
- /// Reason
- /// Variant
- /// Error message
- /// Flag metadata
- public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string? reason, string? variant,
- string? errorMessage = null, ImmutableMetadata? flagMetadata = null)
- {
- this.Value = value;
- this.FlagKey = flagKey;
- this.ErrorType = errorType;
- this.Reason = reason;
- this.Variant = variant;
- this.ErrorMessage = errorMessage;
- this.FlagMetadata = flagMetadata;
- }
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Feature flag key
+ /// Evaluated value
+ /// Error
+ /// Reason
+ /// Variant
+ /// Error message
+ /// Flag metadata
+ public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string? reason, string? variant,
+ string? errorMessage = null, ImmutableMetadata? flagMetadata = null)
+ {
+ this.Value = value;
+ this.FlagKey = flagKey;
+ this.ErrorType = errorType;
+ this.Reason = reason;
+ this.Variant = variant;
+ this.ErrorMessage = errorMessage;
+ this.FlagMetadata = flagMetadata;
+ }
}
diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs
index 8bba0aefa..a261a6b36 100644
--- a/src/OpenFeature/Model/FlagEvaluationOptions.cs
+++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs
@@ -1,44 +1,43 @@
using System.Collections.Immutable;
-namespace OpenFeature.Model
+namespace OpenFeature.Model;
+
+///
+/// A structure containing the one or more hooks and hook hints
+/// The hook and hook hints are added to the list of hooks called during the evaluation process
+///
+/// Flag Evaluation Options
+public sealed class FlagEvaluationOptions
{
///
- /// A structure containing the one or more hooks and hook hints
- /// The hook and hook hints are added to the list of hooks called during the evaluation process
+ /// An immutable list of
///
- /// Flag Evaluation Options
- public sealed class FlagEvaluationOptions
- {
- ///
- /// An immutable list of
- ///
- public IImmutableList Hooks { get; }
+ public IImmutableList Hooks { get; }
- ///
- /// An immutable dictionary of hook hints
- ///
- public IImmutableDictionary HookHints { get; }
+ ///
+ /// An immutable dictionary of hook hints
+ ///
+ public IImmutableDictionary HookHints { get; }
- ///
- /// Initializes a new instance of the class.
- ///
- /// An immutable list of hooks to use during evaluation
- /// Optional - a list of hints that are passed through the hook lifecycle
- public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary? hookHints = null)
- {
- this.Hooks = hooks;
- this.HookHints = hookHints ?? ImmutableDictionary.Empty;
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An immutable list of hooks to use during evaluation
+ /// Optional - a list of hints that are passed through the hook lifecycle
+ public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary? hookHints = null)
+ {
+ this.Hooks = hooks;
+ this.HookHints = hookHints ?? ImmutableDictionary.Empty;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// A hook to use during the evaluation
- /// Optional - a list of hints that are passed through the hook lifecycle
- public FlagEvaluationOptions(Hook hook, ImmutableDictionary? hookHints = null)
- {
- this.Hooks = ImmutableList.Create(hook);
- this.HookHints = hookHints ?? ImmutableDictionary.Empty;
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A hook to use during the evaluation
+ /// Optional - a list of hints that are passed through the hook lifecycle
+ public FlagEvaluationOptions(Hook hook, ImmutableDictionary? hookHints = null)
+ {
+ this.Hooks = ImmutableList.Create(hook);
+ this.HookHints = hookHints ?? ImmutableDictionary.Empty;
}
}
diff --git a/src/OpenFeature/Model/HookContext.cs b/src/OpenFeature/Model/HookContext.cs
index 8d99a2836..4abc773cb 100644
--- a/src/OpenFeature/Model/HookContext.cs
+++ b/src/OpenFeature/Model/HookContext.cs
@@ -1,92 +1,91 @@
using System;
using OpenFeature.Constant;
-namespace OpenFeature.Model
+namespace OpenFeature.Model;
+
+///
+/// Context provided to hook execution
+///
+/// Flag value type
+///
+public sealed class HookContext
{
+ private readonly SharedHookContext _shared;
+
///
- /// Context provided to hook execution
+ /// Feature flag being evaluated
///
- /// Flag value type
- ///
- public sealed class HookContext
- {
- private readonly SharedHookContext _shared;
+ public string FlagKey => this._shared.FlagKey;
- ///
- /// Feature flag being evaluated
- ///
- public string FlagKey => this._shared.FlagKey;
-
- ///
- /// Default value if flag fails to be evaluated
- ///
- public T DefaultValue => this._shared.DefaultValue;
+ ///
+ /// Default value if flag fails to be evaluated
+ ///
+ public T DefaultValue => this._shared.DefaultValue;
- ///
- /// The value type of the flag
- ///
- public FlagValueType FlagValueType => this._shared.FlagValueType;
+ ///
+ /// The value type of the flag
+ ///
+ public FlagValueType FlagValueType => this._shared.FlagValueType;
- ///
- /// User defined evaluation context used in the evaluation process
- ///
- ///
- public EvaluationContext EvaluationContext { get; }
+ ///
+ /// User defined evaluation context used in the evaluation process
+ ///
+ ///
+ public EvaluationContext EvaluationContext { get; }
- ///
- /// Client metadata
- ///
- public ClientMetadata ClientMetadata => this._shared.ClientMetadata;
+ ///
+ /// Client metadata
+ ///
+ public ClientMetadata ClientMetadata => this._shared.ClientMetadata;
- ///
- /// Provider metadata
- ///
- public Metadata ProviderMetadata => this._shared.ProviderMetadata;
+ ///
+ /// Provider metadata
+ ///
+ public Metadata ProviderMetadata => this._shared.ProviderMetadata;
- ///
- /// Hook data
- ///
- public HookData Data { get; }
+ ///
+ /// Hook data
+ ///
+ public HookData Data { get; }
- ///
- /// Initialize a new instance of
- ///
- /// Feature flag key
- /// Default value
- /// Flag value type
- /// Client metadata
- /// Provider metadata
- /// Evaluation context
- /// When any of arguments are null
- public HookContext(string? flagKey,
- T defaultValue,
- FlagValueType flagValueType,
- ClientMetadata? clientMetadata,
- Metadata? providerMetadata,
- EvaluationContext? evaluationContext)
- {
- this._shared = new SharedHookContext(
- flagKey, defaultValue, flagValueType, clientMetadata, providerMetadata);
+ ///
+ /// Initialize a new instance of
+ ///
+ /// Feature flag key
+ /// Default value
+ /// Flag value type
+ /// Client metadata
+ /// Provider metadata
+ /// Evaluation context
+ /// When any of arguments are null
+ public HookContext(string? flagKey,
+ T defaultValue,
+ FlagValueType flagValueType,
+ ClientMetadata? clientMetadata,
+ Metadata? providerMetadata,
+ EvaluationContext? evaluationContext)
+ {
+ this._shared = new SharedHookContext(
+ flagKey, defaultValue, flagValueType, clientMetadata, providerMetadata);
- this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
- this.Data = new HookData();
- }
+ this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
+ this.Data = new HookData();
+ }
- internal HookContext(SharedHookContext? sharedHookContext, EvaluationContext? evaluationContext,
- HookData? hookData)
- {
- this._shared = sharedHookContext ?? throw new ArgumentNullException(nameof(sharedHookContext));
- this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
- this.Data = hookData ?? throw new ArgumentNullException(nameof(hookData));
- }
+ internal HookContext(SharedHookContext? sharedHookContext, EvaluationContext? evaluationContext,
+ HookData? hookData)
+ {
+ this._shared = sharedHookContext ?? throw new ArgumentNullException(nameof(sharedHookContext));
+ this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext));
+ this.Data = hookData ?? throw new ArgumentNullException(nameof(hookData));
+ }
- internal HookContext WithNewEvaluationContext(EvaluationContext context)
- {
- return new HookContext(
- this._shared,
- context,
- this.Data
- );
- }
+ internal HookContext WithNewEvaluationContext(EvaluationContext context)
+ {
+ return new HookContext(
+ this._shared,
+ context,
+ this.Data
+ );
}
}
diff --git a/src/OpenFeature/Model/Metadata.cs b/src/OpenFeature/Model/Metadata.cs
index d7c972d7e..44a059ef2 100644
--- a/src/OpenFeature/Model/Metadata.cs
+++ b/src/OpenFeature/Model/Metadata.cs
@@ -1,22 +1,21 @@
-namespace OpenFeature.Model
+namespace OpenFeature.Model;
+
+///
+///