From a95419089d44d4d73efeba909f4b9110036549dc Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Thu, 21 Jul 2022 16:46:10 -0400 Subject: [PATCH 001/316] Update contributors thank you message Signed-off-by: Michael Beemer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ed43899..9458c10b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ var isEnabled = await client.GetBooleanValue("my-feature", false); ## Contributors -Thanks so much to your contributions to the OpenFeature project. +Thanks so much for your contributions to the OpenFeature project. From 670bb81ce490331d7fcdb6d2287de21290186ac1 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 4 Aug 2022 22:02:10 +1000 Subject: [PATCH 002/316] Bug: FlagEvaluationOptions.HookHints should be optional As per requirement 4.5.1 of the specification, the hook hints should be option on the FlagEvaluationOptions constructor. Should default to empty dictionary https://github.com/open-feature/spec/blob/1bbbb361952adfa4a94618ffe66e943719be9e50/specification/hooks.md#requirement-451 Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .../Model/FlagEvaluationOptions.cs | 12 +++--- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 40 ++++++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs index 45430dcf..53aacceb 100644 --- a/src/OpenFeature/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs @@ -23,22 +23,22 @@ public class FlagEvaluationOptions /// Initializes a new instance of the class. /// /// - /// - public FlagEvaluationOptions(IReadOnlyList hooks, IReadOnlyDictionary hookHints) + /// Optional - a list of hints that are passed through the hook lifecycle + public FlagEvaluationOptions(IReadOnlyList hooks, IReadOnlyDictionary hookHints = null) { this.Hooks = hooks; - this.HookHints = hookHints; + this.HookHints = hookHints ?? new Dictionary(); } /// /// Initializes a new instance of the class. /// /// - /// - public FlagEvaluationOptions(Hook hook, IReadOnlyDictionary hookHints) + /// Optional - a list of hints that are passed through the hook lifecycle + public FlagEvaluationOptions(Hook hook, IReadOnlyDictionary hookHints = null) { this.Hooks = new[] { hook }; - this.HookHints = hookHints; + this.HookHints = hookHints ?? new Dictionary(); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 5fdfe764..21cc9b05 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -214,7 +214,6 @@ public async Task Hook_Should_Execute_In_Correct_Order() hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); hook.Verify(x => x.Finally(It.IsAny>(), It.IsAny>()), Times.Once); featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Once); - } [Fact] @@ -367,5 +366,44 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ hook1.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); hook2.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); } + + [Fact] + [Specification("4.5.1", "`Flag evaluation options` MAY contain `hook hints`, a map of data to be provided to hook invocations.")] + public async Task Hook_Hints_May_Be_Optional() + { + var featureProvider = new Mock(); + var hook = new Mock(); + var defaultEmptyHookHints = new Dictionary(); + var flagOptions = new FlagEvaluationOptions(hook.Object); + + var sequence = new MockSequence(); + + featureProvider.Setup(x => x.GetMetadata()) + .Returns(new Metadata(null)); + + hook.InSequence(sequence) + .Setup(x => x.Before(It.IsAny>(), defaultEmptyHookHints)) + .ReturnsAsync(new EvaluationContext()); + + featureProvider.InSequence(sequence) + .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), flagOptions)) + .ReturnsAsync(new ResolutionDetails("test", false)); + + hook.InSequence(sequence) + .Setup(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints)); + + hook.InSequence(sequence) + .Setup(x => x.Finally(It.IsAny>(), defaultEmptyHookHints)); + + OpenFeature.Instance.SetProvider(featureProvider.Object); + var client = OpenFeature.Instance.GetClient(); + + await client.GetBooleanValue("test", false, config: flagOptions); + + hook.Verify(x => x.Before(It.IsAny>(), defaultEmptyHookHints), Times.Once); + hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints), Times.Once); + hook.Verify(x => x.Finally(It.IsAny>(), defaultEmptyHookHints), Times.Once); + featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), flagOptions), Times.Once); + } } } From 778fa9b2d85a81e57b6f264e4405e4420023a577 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 4 Aug 2022 22:41:56 +1000 Subject: [PATCH 003/316] Feat: Implement seperate methods for integer and double - Add GetDoubleValue, GetDoubleDetails and ResolveDoubleValue to FeatureProvider interface and FeatureClient - Rename GetNumberValue, GetNumberDetails to GetIntegerValue, GetIntegerDetails - Add tests for new methods Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- src/OpenFeature/IFeatureClient.cs | 7 +++- src/OpenFeature/IFeatureProvider.cs | 15 ++++++- src/OpenFeature/NoOpProvider.cs | 8 +++- src/OpenFeature/OpenFeatureClient.cs | 38 +++++++++++++++--- .../OpenFeature.Tests/FeatureProviderTests.cs | 26 ++++++++---- .../OpenFeatureClientTests.cs | 40 ++++++++++++------- test/OpenFeature.Tests/TestImplementations.cs | 9 ++++- 7 files changed, 110 insertions(+), 33 deletions(-) diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index cb0f7c62..8150f3e7 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -15,8 +15,11 @@ internal interface IFeatureClient Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); - Task GetNumberValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); - Task> GetNumberDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + + Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); Task GetObjectValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); Task> GetObjectDetails(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); diff --git a/src/OpenFeature/IFeatureProvider.cs b/src/OpenFeature/IFeatureProvider.cs index 303832c1..5a463eb5 100644 --- a/src/OpenFeature/IFeatureProvider.cs +++ b/src/OpenFeature/IFeatureProvider.cs @@ -39,14 +39,25 @@ Task> ResolveStringValue(string flagKey, string defaul EvaluationContext context = null, FlagEvaluationOptions config = null); /// - /// Resolves a number feature flag + /// Resolves a integer feature flag /// /// Feature flag key /// Default value /// /// /// - Task> ResolveNumberValue(string flagKey, int defaultValue, + Task> ResolveIntegerValue(string flagKey, int defaultValue, + EvaluationContext context = null, FlagEvaluationOptions config = null); + + /// + /// Resolves a double feature flag + /// + /// Feature flag key + /// Default value + /// + /// + /// + Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); /// diff --git a/src/OpenFeature/NoOpProvider.cs b/src/OpenFeature/NoOpProvider.cs index f9e0dde7..9ad298c2 100644 --- a/src/OpenFeature/NoOpProvider.cs +++ b/src/OpenFeature/NoOpProvider.cs @@ -23,7 +23,13 @@ public Task> ResolveStringValue(string flagKey, string return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public Task> ResolveNumberValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, + FlagEvaluationOptions config = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index e925dd58..0a54df3d 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -116,28 +116,54 @@ await this.EvaluateFlag(this._featureProvider.ResolveStringValue, FlagValueType. defaultValue, context, config); /// - /// Resolves a number feature flag + /// Resolves a integer feature flag /// /// Feature flag key /// Default value /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - public async Task GetNumberValue(string flagKey, int defaultValue, EvaluationContext context = null, + public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - (await this.GetNumberDetails(flagKey, defaultValue, context, config)).Value; + (await this.GetIntegerDetails(flagKey, defaultValue, context, config)).Value; /// - /// Resolves a number feature flag + /// Resolves a integer feature flag /// /// Feature flag key /// Default value /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - public async Task> GetNumberDetails(string flagKey, int defaultValue, + public async Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - await this.EvaluateFlag(this._featureProvider.ResolveNumberValue, FlagValueType.Number, flagKey, + await this.EvaluateFlag(this._featureProvider.ResolveIntegerValue, FlagValueType.Number, flagKey, + defaultValue, context, config); + + /// + /// Resolves a double feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag details + public async Task GetDoubleValue(string flagKey, double defaultValue, + EvaluationContext context = null, + FlagEvaluationOptions config = null) => + (await this.GetDoubleDetails(flagKey, defaultValue, context, config)).Value; + + /// + /// Resolves a double feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag details + public async Task> GetDoubleDetails(string flagKey, double defaultValue, + EvaluationContext context = null, FlagEvaluationOptions config = null) => + await this.EvaluateFlag(this._featureProvider.ResolveDoubleValue, FlagValueType.Number, flagKey, defaultValue, context, config); /// diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index 29bfa44f..0a48205d 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -34,16 +34,23 @@ public async Task Provider_Must_Resolve_Flag_Values() var flagName = fixture.Create(); var defaultBoolValue = fixture.Create(); var defaultStringValue = fixture.Create(); - var defaultNumberValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); var provider = new NoOpFeatureProvider(); var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await provider.ResolveBooleanValue(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolResolutionDetails); - var numberResolutionDetails = new ResolutionDetails(flagName, defaultNumberValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveNumberValue(flagName, defaultNumberValue)).Should().BeEquivalentTo(numberResolutionDetails); + + var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await provider.ResolveIntegerValue(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerResolutionDetails); + + var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await provider.ResolveDoubleValue(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleResolutionDetails); + var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await provider.ResolveStringValue(flagName, defaultStringValue)).Should().BeEquivalentTo(stringResolutionDetails); + var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await provider.ResolveStructureValue(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureResolutionDetails); } @@ -57,15 +64,19 @@ public async Task Provider_Must_ErrorType() var flagName2 = fixture.Create(); var defaultBoolValue = fixture.Create(); var defaultStringValue = fixture.Create(); - var defaultNumberValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); var providerMock = new Mock(); providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - providerMock.Setup(x => x.ResolveNumberValue(flagName, defaultNumberValue, It.IsAny(), It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultNumberValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + providerMock.Setup(x => x.ResolveIntegerValue(flagName, defaultIntegerValue, It.IsAny(), It.IsAny())) + .ReturnsAsync(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + + providerMock.Setup(x => x.ResolveDoubleValue(flagName, defaultDoubleValue, It.IsAny(), It.IsAny())) + .ReturnsAsync(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); providerMock.Setup(x => x.ResolveStringValue(flagName, defaultStringValue, It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); @@ -79,7 +90,8 @@ public async Task Provider_Must_ErrorType() var provider = providerMock.Object; (await provider.ResolveBooleanValue(flagName, defaultBoolValue)).ErrorType.Should().Be(ErrorType.General); - (await provider.ResolveNumberValue(flagName, defaultNumberValue)).ErrorType.Should().Be(ErrorType.ParseError); + (await provider.ResolveIntegerValue(flagName, defaultIntegerValue)).ErrorType.Should().Be(ErrorType.ParseError); + (await provider.ResolveDoubleValue(flagName, defaultDoubleValue)).ErrorType.Should().Be(ErrorType.ParseError); (await provider.ResolveStringValue(flagName, defaultStringValue)).ErrorType.Should().Be(ErrorType.TypeMismatch); (await provider.ResolveStructureValue(flagName, defaultStructureValue)).ErrorType.Should().Be(ErrorType.FlagNotFound); (await provider.ResolveStructureValue(flagName2, defaultStructureValue)).ErrorType.Should().Be(ErrorType.ProviderNotReady); diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 9646ae96..6319aa10 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -55,8 +55,9 @@ public void OpenFeatureClient_Metadata_Should_Have_Name() } [Fact] - [Specification("1.3.1", "The `client` MUST provide methods for flag evaluation, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns the flag value.")] - [Specification("1.3.2.1", "The `client` MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure.")] + [Specification("1.3.1", "The `client` MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns the flag value.")] + [Specification("1.3.2.1", "he client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")] + [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() { var fixture = new Fixture(); @@ -65,7 +66,8 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var flagName = fixture.Create(); var defaultBoolValue = fixture.Create(); var defaultStringValue = fixture.Create(); - var defaultNumberValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); @@ -76,9 +78,13 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() (await client.GetBooleanValue(flagName, defaultBoolValue, new EvaluationContext())).Should().Be(defaultBoolValue); (await client.GetBooleanValue(flagName, defaultBoolValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultBoolValue); - (await client.GetNumberValue(flagName, defaultNumberValue)).Should().Be(defaultNumberValue); - (await client.GetNumberValue(flagName, defaultNumberValue, new EvaluationContext())).Should().Be(defaultNumberValue); - (await client.GetNumberValue(flagName, defaultNumberValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultNumberValue); + (await client.GetIntegerValue(flagName, defaultIntegerValue)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValue(flagName, defaultIntegerValue, new EvaluationContext())).Should().Be(defaultIntegerValue); + (await client.GetIntegerValue(flagName, defaultIntegerValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultIntegerValue); + + (await client.GetDoubleValue(flagName, defaultDoubleValue)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValue(flagName, defaultDoubleValue, new EvaluationContext())).Should().Be(defaultDoubleValue); + (await client.GetDoubleValue(flagName, defaultDoubleValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultDoubleValue); (await client.GetStringValue(flagName, defaultStringValue)).Should().Be(defaultStringValue); (await client.GetStringValue(flagName, defaultStringValue, new EvaluationContext())).Should().Be(defaultStringValue); @@ -106,7 +112,8 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var flagName = fixture.Create(); var defaultBoolValue = fixture.Create(); var defaultStringValue = fixture.Create(); - var defaultNumberValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); @@ -118,10 +125,15 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() (await client.GetBooleanDetails(flagName, defaultBoolValue, new EvaluationContext())).Should().BeEquivalentTo(boolFlagEvaluationDetails); (await client.GetBooleanDetails(flagName, defaultBoolValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - var numberFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultNumberValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetNumberDetails(flagName, defaultNumberValue)).Should().BeEquivalentTo(numberFlagEvaluationDetails); - (await client.GetNumberDetails(flagName, defaultNumberValue, new EvaluationContext())).Should().BeEquivalentTo(numberFlagEvaluationDetails); - (await client.GetNumberDetails(flagName, defaultNumberValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(numberFlagEvaluationDetails); + var integerFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await client.GetIntegerDetails(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetails(flagName, defaultIntegerValue, new EvaluationContext())).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetails(flagName, defaultIntegerValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + + var doubleFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await client.GetDoubleDetails(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetails(flagName, defaultDoubleValue, new EvaluationContext())).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetails(flagName, defaultDoubleValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); var stringFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetStringDetails(flagName, defaultStringValue)).Should().BeEquivalentTo(stringFlagEvaluationDetails); @@ -236,7 +248,7 @@ public async Task Should_Resolve_NumberValue() var featureProviderMock = new Mock(); featureProviderMock - .Setup(x => x.ResolveNumberValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny(), null)) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -244,9 +256,9 @@ public async Task Should_Resolve_NumberValue() OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); - (await client.GetNumberValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveNumberValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + featureProviderMock.Verify(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny(), null), Times.Once); } [Fact] diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index c731650b..0898100f 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -60,7 +60,14 @@ public Task> ResolveStringValue(string flagKey, string throw new NotImplementedException(); } - public Task> ResolveNumberValue(string flagKey, int defaultValue, + public Task> ResolveIntegerValue(string flagKey, int defaultValue, + EvaluationContext context = null, + FlagEvaluationOptions config = null) + { + throw new NotImplementedException(); + } + + public Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { From 291455429faa3ef714420ced0ffc1ec44da507de Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 4 Aug 2022 23:27:44 +1000 Subject: [PATCH 004/316] Chore: Evaluation Context must only contain unique values https://github.com/open-feature/spec/pull/120 Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .../OpenFeatureEvaluationContextTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 82dc034b..5973aa09 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -27,6 +27,7 @@ public void Should_Merge_Two_Contexts() } [Fact] + [Specification("3.2.2", "Duplicate values being overwritten.")] public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Context() { var context1 = new EvaluationContext(); @@ -77,5 +78,15 @@ public void EvaluationContext_Should_All_Types() context.Get("key4").Should().Be(now); context.Get("key5").Should().Be(structure); } + + [Fact] + [Specification("3.1.4", "The evaluation context fields MUST have an unique key.")] + public void When_Duplicate_Key_Throw_Unique_Constraint() + { + var context = new EvaluationContext { { "key", "value" } }; + var exception = Assert.Throws(() => + context.Add("key", "overriden_value")); + exception.Message.Should().StartWith("An item with the same key has already been added."); + } } } From 7ab12ea21f307043fb9b5f1b3950a2056a5eae70 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:53:45 +1000 Subject: [PATCH 005/316] Chore: Add basic issue templates Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug.yaml | 23 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.yaml | 11 +++++++++++ .github/ISSUE_TEMPLATE/feature.yaml | 14 ++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature.yaml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 00000000..8dc048af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,23 @@ +name: 🐞 Bug +description: Found a bug? We are sorry about that! Let us know! πŸ› +title: "[BUG] " +labels: [bug, Needs Triage] +body: +- type: textarea + attributes: + label: Observed behavior + description: What are you trying to do? Describe what you think went wrong during this. + validations: + required: false +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false +- type: textarea + attributes: + label: Steps to reproduce + description: Describe as best you can the problem. Please provide us scenario file, logs or anything you have that can help us to understand. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml new file mode 100644 index 00000000..3c20743a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yaml @@ -0,0 +1,11 @@ +name: πŸ““ Documentation +description: Any documentation related issue/addition. +title: "[DOC] " +labels: [documentation, Needs Triage] +body: +- type: textarea + attributes: + label: Change in the documentation + description: What should we add/remove/update in the documentation? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 00000000..034e3d65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,14 @@ +name: πŸ’‘ Feature +description: Add new functionality to the project. +title: "[FEATURE] " +labels: [enhancement, Needs Triage] +body: +- type: textarea + attributes: + label: Requirements + description: | + Ask us what you want! Please provide as many details as possible and describe how it should work. + + Note: Spec and architecture changes require an [OFEP](https://github.com/open-feature/research). + validations: + required: false From 7ff19565fa32d95c3e34d6ba22d2ee1be0486204 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 4 Aug 2022 22:19:33 +1000 Subject: [PATCH 006/316] Chore: Add test to cover spec 4.4.7 When error is thrown during calling a before hook the default value should be returned https://github.com/open-feature/spec/pull/110 Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 21cc9b05..da75ee61 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -405,5 +405,39 @@ public async Task Hook_Hints_May_Be_Optional() hook.Verify(x => x.Finally(It.IsAny>(), defaultEmptyHookHints), Times.Once); featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), flagOptions), Times.Once); } + + [Fact] + [Specification("4.4.7", "If an error occurs in the `before` hooks, the default value MUST be returned.")] + public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() + { + var featureProvider = new Mock(); + var hook = new Mock(); + var exceptionToThrow = new Exception("Fails during default"); + + var sequence = new MockSequence(); + + featureProvider.Setup(x => x.GetMetadata()) + .Returns(new Metadata(null)); + + hook.InSequence(sequence) + .Setup(x => x.Before(It.IsAny>(), It.IsAny>())) + .ThrowsAsync(exceptionToThrow); + + hook.InSequence(sequence) + .Setup(x => x.Error(It.IsAny>(), It.IsAny(), null)); + + hook.InSequence(sequence) + .Setup(x => x.Finally(It.IsAny>(), null)); + + var client = OpenFeature.Instance.GetClient(); + client.AddHooks(hook.Object); + + var resolvedFlag = await client.GetBooleanValue("test", true); + + resolvedFlag.Should().BeTrue(); + hook.Verify(x => x.Before(It.IsAny>(), null), Times.Once); + hook.Verify(x => x.Error(It.IsAny>(), exceptionToThrow, null), Times.Once); + hook.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); + } } } From 355bebcaa77d85bfa05443c0f6c6b616b12ebbfd Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 5 Aug 2022 22:16:45 +1000 Subject: [PATCH 007/316] Chore: Name the solution and project OpenFeature.SDK Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- OpenFeature.proj => OpenFeature.SDK.proj | 0 OpenFeature.sln => OpenFeature.SDK.sln | 11 +++++++++-- build/Common.tests.props | 2 +- src/Directory.Build.props | 2 +- .../Constant/ErrorType.cs | 0 .../Constant/FlagValueType.cs | 0 .../Constant/NoOpProvider.cs | 0 .../Constant/Reason.cs | 0 .../Error/FeatureProviderException.cs | 0 .../Extension/EnumExtensions.cs | 0 .../Extension/ResolutionDetailsExtensions.cs | 0 src/{OpenFeature => OpenFeature.SDK}/Hook.cs | 0 .../IFeatureClient.cs | 0 .../IFeatureProvider.cs | 0 .../Model/ClientMetadata.cs | 0 .../Model/EvaluationContext.cs | 0 .../Model/FlagEvaluationOptions.cs | 0 .../Model/FlagEvalusationDetails.cs | 0 .../Model/HookContext.cs | 0 .../Model/Metadata.cs | 0 .../Model/ResolutionDetails.cs | 0 src/{OpenFeature => OpenFeature.SDK}/NoOpProvider.cs | 0 .../OpenFeature.SDK.csproj} | 3 ++- src/{OpenFeature => OpenFeature.SDK}/OpenFeature.cs | 0 .../OpenFeatureClient.cs | 0 test/Directory.Build.props | 2 +- .../FeatureProviderExceptionTests.cs | 0 .../FeatureProviderTests.cs | 0 .../Internal/SpecificationAttribute.cs | 0 .../OpenFeature.SDK.Tests.csproj} | 3 ++- .../OpenFeatureClientTests.cs | 0 .../OpenFeatureEvaluationContextTests.cs | 0 .../OpenFeatureHookTests.cs | 0 .../OpenFeatureTests.cs | 0 .../TestImplementations.cs | 0 35 files changed, 16 insertions(+), 7 deletions(-) rename OpenFeature.proj => OpenFeature.SDK.proj (100%) rename OpenFeature.sln => OpenFeature.SDK.sln (75%) rename src/{OpenFeature => OpenFeature.SDK}/Constant/ErrorType.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Constant/FlagValueType.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Constant/NoOpProvider.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Constant/Reason.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Error/FeatureProviderException.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Extension/EnumExtensions.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Extension/ResolutionDetailsExtensions.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Hook.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/IFeatureClient.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/IFeatureProvider.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Model/ClientMetadata.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Model/EvaluationContext.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Model/FlagEvaluationOptions.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Model/FlagEvalusationDetails.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Model/HookContext.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Model/Metadata.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/Model/ResolutionDetails.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/NoOpProvider.cs (100%) rename src/{OpenFeature/OpenFeature.csproj => OpenFeature.SDK/OpenFeature.SDK.csproj} (79%) rename src/{OpenFeature => OpenFeature.SDK}/OpenFeature.cs (100%) rename src/{OpenFeature => OpenFeature.SDK}/OpenFeatureClient.cs (100%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/FeatureProviderExceptionTests.cs (100%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/FeatureProviderTests.cs (100%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/Internal/SpecificationAttribute.cs (100%) rename test/{OpenFeature.Tests/OpenFeature.Tests.csproj => OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj} (91%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/OpenFeatureClientTests.cs (100%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/OpenFeatureEvaluationContextTests.cs (100%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/OpenFeatureHookTests.cs (100%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/OpenFeatureTests.cs (100%) rename test/{OpenFeature.Tests => OpenFeature.SDK.Tests}/TestImplementations.cs (100%) diff --git a/OpenFeature.proj b/OpenFeature.SDK.proj similarity index 100% rename from OpenFeature.proj rename to OpenFeature.SDK.proj diff --git a/OpenFeature.sln b/OpenFeature.SDK.sln similarity index 75% rename from OpenFeature.sln rename to OpenFeature.SDK.sln index e20ad90f..cd7aea08 100644 --- a/OpenFeature.sln +++ b/OpenFeature.SDK.sln @@ -1,6 +1,6 @@ ο»Ώ Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.SDK", "src\OpenFeature.SDK\OpenFeature.SDK.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}" ProjectSection(SolutionItems) = preProject @@ -8,6 +8,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60 build\Common.tests.props = build\Common.tests.props build\Common.prod.props = build\Common.prod.props build\RELEASING.md = build\RELEASING.md + .github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\dotnet-format.yml = .github\workflows\dotnet-format.yml + .github\workflows\linux-ci.yml = .github\workflows\linux-ci.yml + .github\workflows\prerelease-package.yml = .github\workflows\prerelease-package.yml + .github\workflows\release-package.yml = .github\workflows\release-package.yml + .github\workflows\windows-ci.yml = .github\workflows\windows-ci.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C97E9975-E10A-4817-AE2C-4DD69C3C02D4}" @@ -21,7 +28,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{65FBA159-2 test\Directory.Build.props = test\Directory.Build.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Tests", "test\OpenFeature.Tests\OpenFeature.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.SDK.Tests", "test\OpenFeature.SDK.Tests\OpenFeature.SDK.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/build/Common.tests.props b/build/Common.tests.props index d4a6454e..4cdbe034 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -10,7 +10,7 @@ - + PreserveNewest diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e9839283..157b717e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,3 +1,3 @@ - + diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature.SDK/Constant/ErrorType.cs similarity index 100% rename from src/OpenFeature/Constant/ErrorType.cs rename to src/OpenFeature.SDK/Constant/ErrorType.cs diff --git a/src/OpenFeature/Constant/FlagValueType.cs b/src/OpenFeature.SDK/Constant/FlagValueType.cs similarity index 100% rename from src/OpenFeature/Constant/FlagValueType.cs rename to src/OpenFeature.SDK/Constant/FlagValueType.cs diff --git a/src/OpenFeature/Constant/NoOpProvider.cs b/src/OpenFeature.SDK/Constant/NoOpProvider.cs similarity index 100% rename from src/OpenFeature/Constant/NoOpProvider.cs rename to src/OpenFeature.SDK/Constant/NoOpProvider.cs diff --git a/src/OpenFeature/Constant/Reason.cs b/src/OpenFeature.SDK/Constant/Reason.cs similarity index 100% rename from src/OpenFeature/Constant/Reason.cs rename to src/OpenFeature.SDK/Constant/Reason.cs diff --git a/src/OpenFeature/Error/FeatureProviderException.cs b/src/OpenFeature.SDK/Error/FeatureProviderException.cs similarity index 100% rename from src/OpenFeature/Error/FeatureProviderException.cs rename to src/OpenFeature.SDK/Error/FeatureProviderException.cs diff --git a/src/OpenFeature/Extension/EnumExtensions.cs b/src/OpenFeature.SDK/Extension/EnumExtensions.cs similarity index 100% rename from src/OpenFeature/Extension/EnumExtensions.cs rename to src/OpenFeature.SDK/Extension/EnumExtensions.cs diff --git a/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs b/src/OpenFeature.SDK/Extension/ResolutionDetailsExtensions.cs similarity index 100% rename from src/OpenFeature/Extension/ResolutionDetailsExtensions.cs rename to src/OpenFeature.SDK/Extension/ResolutionDetailsExtensions.cs diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature.SDK/Hook.cs similarity index 100% rename from src/OpenFeature/Hook.cs rename to src/OpenFeature.SDK/Hook.cs diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature.SDK/IFeatureClient.cs similarity index 100% rename from src/OpenFeature/IFeatureClient.cs rename to src/OpenFeature.SDK/IFeatureClient.cs diff --git a/src/OpenFeature/IFeatureProvider.cs b/src/OpenFeature.SDK/IFeatureProvider.cs similarity index 100% rename from src/OpenFeature/IFeatureProvider.cs rename to src/OpenFeature.SDK/IFeatureProvider.cs diff --git a/src/OpenFeature/Model/ClientMetadata.cs b/src/OpenFeature.SDK/Model/ClientMetadata.cs similarity index 100% rename from src/OpenFeature/Model/ClientMetadata.cs rename to src/OpenFeature.SDK/Model/ClientMetadata.cs diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature.SDK/Model/EvaluationContext.cs similarity index 100% rename from src/OpenFeature/Model/EvaluationContext.cs rename to src/OpenFeature.SDK/Model/EvaluationContext.cs diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature.SDK/Model/FlagEvaluationOptions.cs similarity index 100% rename from src/OpenFeature/Model/FlagEvaluationOptions.cs rename to src/OpenFeature.SDK/Model/FlagEvaluationOptions.cs diff --git a/src/OpenFeature/Model/FlagEvalusationDetails.cs b/src/OpenFeature.SDK/Model/FlagEvalusationDetails.cs similarity index 100% rename from src/OpenFeature/Model/FlagEvalusationDetails.cs rename to src/OpenFeature.SDK/Model/FlagEvalusationDetails.cs diff --git a/src/OpenFeature/Model/HookContext.cs b/src/OpenFeature.SDK/Model/HookContext.cs similarity index 100% rename from src/OpenFeature/Model/HookContext.cs rename to src/OpenFeature.SDK/Model/HookContext.cs diff --git a/src/OpenFeature/Model/Metadata.cs b/src/OpenFeature.SDK/Model/Metadata.cs similarity index 100% rename from src/OpenFeature/Model/Metadata.cs rename to src/OpenFeature.SDK/Model/Metadata.cs diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature.SDK/Model/ResolutionDetails.cs similarity index 100% rename from src/OpenFeature/Model/ResolutionDetails.cs rename to src/OpenFeature.SDK/Model/ResolutionDetails.cs diff --git a/src/OpenFeature/NoOpProvider.cs b/src/OpenFeature.SDK/NoOpProvider.cs similarity index 100% rename from src/OpenFeature/NoOpProvider.cs rename to src/OpenFeature.SDK/NoOpProvider.cs diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature.SDK/OpenFeature.SDK.csproj similarity index 79% rename from src/OpenFeature/OpenFeature.csproj rename to src/OpenFeature.SDK/OpenFeature.SDK.csproj index ca68ee2e..4f479f01 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature.SDK/OpenFeature.SDK.csproj @@ -2,6 +2,7 @@ netstandard2.0;net462 + OpenFeature.SDK @@ -10,7 +11,7 @@ - <_Parameter1>OpenFeature.Tests + <_Parameter1>OpenFeature.SDK.Tests diff --git a/src/OpenFeature/OpenFeature.cs b/src/OpenFeature.SDK/OpenFeature.cs similarity index 100% rename from src/OpenFeature/OpenFeature.cs rename to src/OpenFeature.SDK/OpenFeature.cs diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs similarity index 100% rename from src/OpenFeature/OpenFeatureClient.cs rename to src/OpenFeature.SDK/OpenFeatureClient.cs diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 1487b265..3ed2d6f4 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,3 +1,3 @@ - + diff --git a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeature.SDK.Tests/FeatureProviderExceptionTests.cs similarity index 100% rename from test/OpenFeature.Tests/FeatureProviderExceptionTests.cs rename to test/OpenFeature.SDK.Tests/FeatureProviderExceptionTests.cs diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs similarity index 100% rename from test/OpenFeature.Tests/FeatureProviderTests.cs rename to test/OpenFeature.SDK.Tests/FeatureProviderTests.cs diff --git a/test/OpenFeature.Tests/Internal/SpecificationAttribute.cs b/test/OpenFeature.SDK.Tests/Internal/SpecificationAttribute.cs similarity index 100% rename from test/OpenFeature.Tests/Internal/SpecificationAttribute.cs rename to test/OpenFeature.SDK.Tests/Internal/SpecificationAttribute.cs diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj similarity index 91% rename from test/OpenFeature.Tests/OpenFeature.Tests.csproj rename to test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj index 06639880..f9765bb6 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj @@ -3,6 +3,7 @@ net6.0 $(TargetFrameworks);net462 + OpenFeature.SDK.Tests @@ -26,7 +27,7 @@ - + diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs similarity index 100% rename from test/OpenFeature.Tests/OpenFeatureClientTests.cs rename to test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs similarity index 100% rename from test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs rename to test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs similarity index 100% rename from test/OpenFeature.Tests/OpenFeatureHookTests.cs rename to test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureTests.cs similarity index 100% rename from test/OpenFeature.Tests/OpenFeatureTests.cs rename to test/OpenFeature.SDK.Tests/OpenFeatureTests.cs diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.SDK.Tests/TestImplementations.cs similarity index 100% rename from test/OpenFeature.Tests/TestImplementations.cs rename to test/OpenFeature.SDK.Tests/TestImplementations.cs From dad5a0c5272d5687464c8067e27898c174ed3f8f Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sun, 7 Aug 2022 23:40:27 +1000 Subject: [PATCH 008/316] Chore: Add github action to perform release This action will trigger when a new tag is push match {major}.{minor}.{patch} semver format. Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .config/dotnet-tools.json | 12 +++++++++ .github/workflows/release-package.yml | 36 +++++++++++++++++++++++++++ OpenFeature.SDK.sln | 1 - 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .github/workflows/release-package.yml diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..0bbbe55c --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "minver-cli": { + "version": "4.1.0", + "commands": [ + "minver" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml new file mode 100644 index 00000000..9672c8ed --- /dev/null +++ b/.github/workflows/release-package.yml @@ -0,0 +1,36 @@ +name: Release Package + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + release-package: + runs-on: windows-latest + + strategy: + matrix: + version: [net462,net6.0] + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Restore Tools + run: dotnet tool restore + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore -p:Deterministic=true + + - name: Pack + run: dotnet pack OpenFeature.proj --configuration Release --no-build + + - name: Publish to Nuget + run: | + VERSION=$(dotnet minver -v e) + dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json diff --git a/OpenFeature.SDK.sln b/OpenFeature.SDK.sln index cd7aea08..cc483c05 100644 --- a/OpenFeature.SDK.sln +++ b/OpenFeature.SDK.sln @@ -12,7 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60 .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml .github\workflows\dotnet-format.yml = .github\workflows\dotnet-format.yml .github\workflows\linux-ci.yml = .github\workflows\linux-ci.yml - .github\workflows\prerelease-package.yml = .github\workflows\prerelease-package.yml .github\workflows\release-package.yml = .github\workflows\release-package.yml .github\workflows\windows-ci.yml = .github\workflows\windows-ci.yml EndProjectSection From 46b093d7d542591d5fa926ecdc036ae8b3701dc1 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Tue, 9 Aug 2022 20:31:03 +1000 Subject: [PATCH 009/316] Prefix tag must have v Co-authored-by: Michael Beemer Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .github/workflows/release-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 9672c8ed..96629a3d 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -3,7 +3,7 @@ name: Release Package on: push: tags: - - '[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release-package: From 8cb5f6b9b683f004c34dea384453324947ee8460 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sat, 6 Aug 2022 12:36:49 +1000 Subject: [PATCH 010/316] Feat: Add support for provider hooks - [Breaking] Remove IFeatureProvider interface infavor of FeatureProvider abstract class so default implementations can exist - Make sure Provider hooks are called in the correct order - Use strick mocking mode so sequence is validated correctly - Update test cases for provider hooks support Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- ...IFeatureProvider.cs => FeatureProvider.cs} | 29 ++- .../Model/ResolutionDetails.cs | 2 +- src/OpenFeature.SDK/NoOpProvider.cs | 14 +- src/OpenFeature.SDK/OpenFeature.cs | 12 +- src/OpenFeature.SDK/OpenFeatureClient.cs | 7 +- .../ClearOpenFeatureInstanceFixture.cs | 13 ++ .../FeatureProviderTests.cs | 4 +- .../OpenFeatureClientTests.cs | 62 +++++- .../OpenFeatureHookTests.cs | 207 ++++++++++++------ .../OpenFeature.SDK.Tests/OpenFeatureTests.cs | 14 +- .../TestImplementations.cs | 30 ++- 11 files changed, 268 insertions(+), 126 deletions(-) rename src/OpenFeature.SDK/{IFeatureProvider.cs => FeatureProvider.cs} (70%) create mode 100644 test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs diff --git a/src/OpenFeature.SDK/IFeatureProvider.cs b/src/OpenFeature.SDK/FeatureProvider.cs similarity index 70% rename from src/OpenFeature.SDK/IFeatureProvider.cs rename to src/OpenFeature.SDK/FeatureProvider.cs index 5a463eb5..ef767b4a 100644 --- a/src/OpenFeature.SDK/IFeatureProvider.cs +++ b/src/OpenFeature.SDK/FeatureProvider.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Threading.Tasks; using OpenFeature.SDK.Model; @@ -8,13 +10,26 @@ namespace OpenFeature.SDK /// A provider acts as the translates layer between the generic feature flag structure to a target feature flag system. /// /// Provider specification - public interface IFeatureProvider + public abstract class FeatureProvider { + /// + /// Gets a immutable list of hooks that belong to the provider. + /// By default return a 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 + /// + /// + public virtual IReadOnlyList GetProviderHooks() => Array.Empty(); + /// /// Metadata describing the provider. /// /// - Metadata GetMetadata(); + public abstract Metadata GetMetadata(); /// /// Resolves a boolean feature flag @@ -24,7 +39,7 @@ public interface IFeatureProvider /// /// /// - Task> ResolveBooleanValue(string flagKey, bool defaultValue, + public abstract Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); /// @@ -35,7 +50,7 @@ Task> ResolveBooleanValue(string flagKey, bool defaultVa /// /// /// - Task> ResolveStringValue(string flagKey, string defaultValue, + public abstract Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); /// @@ -46,7 +61,7 @@ Task> ResolveStringValue(string flagKey, string defaul /// /// /// - Task> ResolveIntegerValue(string flagKey, int defaultValue, + public abstract Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); /// @@ -57,7 +72,7 @@ Task> ResolveIntegerValue(string flagKey, int defaultValu /// /// /// - Task> ResolveDoubleValue(string flagKey, double defaultValue, + public abstract Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); /// @@ -69,7 +84,7 @@ Task> ResolveDoubleValue(string flagKey, double defaul /// /// Type of object /// - Task> ResolveStructureValue(string flagKey, T defaultValue, + public abstract Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); } } diff --git a/src/OpenFeature.SDK/Model/ResolutionDetails.cs b/src/OpenFeature.SDK/Model/ResolutionDetails.cs index 98313f9e..4672327f 100644 --- a/src/OpenFeature.SDK/Model/ResolutionDetails.cs +++ b/src/OpenFeature.SDK/Model/ResolutionDetails.cs @@ -3,7 +3,7 @@ namespace OpenFeature.SDK.Model { /// - /// Defines the contract that the is required to return + /// Defines the contract that the is required to return /// Describes the details of the feature flag being evaluated /// /// Flag value type diff --git a/src/OpenFeature.SDK/NoOpProvider.cs b/src/OpenFeature.SDK/NoOpProvider.cs index 9ad298c2..0b5d621b 100644 --- a/src/OpenFeature.SDK/NoOpProvider.cs +++ b/src/OpenFeature.SDK/NoOpProvider.cs @@ -4,37 +4,37 @@ namespace OpenFeature.SDK { - internal class NoOpFeatureProvider : IFeatureProvider + internal class NoOpFeatureProvider : FeatureProvider { private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); - public Metadata GetMetadata() + public override Metadata GetMetadata() { return this._metadata; } - public Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature.SDK/OpenFeature.cs b/src/OpenFeature.SDK/OpenFeature.cs index 66f04612..8099299b 100644 --- a/src/OpenFeature.SDK/OpenFeature.cs +++ b/src/OpenFeature.SDK/OpenFeature.cs @@ -12,7 +12,7 @@ namespace OpenFeature.SDK public sealed class OpenFeature { private EvaluationContext _evaluationContext = new EvaluationContext(); - private IFeatureProvider _featureProvider = new NoOpFeatureProvider(); + private FeatureProvider _featureProvider = new NoOpFeatureProvider(); private readonly List _hooks = new List(); /// @@ -29,14 +29,14 @@ private OpenFeature() { } /// /// Sets the feature provider /// - /// Implementation of - public void SetProvider(IFeatureProvider featureProvider) => this._featureProvider = featureProvider; + /// Implementation of + public void SetProvider(FeatureProvider featureProvider) => this._featureProvider = featureProvider; /// /// Gets the feature provider /// - /// - public IFeatureProvider GetProvider() => this._featureProvider; + /// + public FeatureProvider GetProvider() => this._featureProvider; /// /// Gets providers metadata @@ -81,7 +81,7 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge /// Sets the global /// /// - public void SetContext(EvaluationContext context) => this._evaluationContext = context; + public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? new EvaluationContext(); /// /// Gets the global diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs index 0a54df3d..35904823 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeature.SDK/OpenFeatureClient.cs @@ -17,19 +17,19 @@ namespace OpenFeature.SDK public sealed class FeatureClient : IFeatureClient { private readonly ClientMetadata _metadata; - private readonly IFeatureProvider _featureProvider; + private readonly FeatureProvider _featureProvider; private readonly List _hooks = new List(); private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// - /// Feature provider used by client + /// Feature provider used by client /// Name of client /// Version of client /// Logger used by client /// Throws if any of the required parameters are null - public FeatureClient(IFeatureProvider featureProvider, string name, string version, ILogger logger = null) + public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null) { this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider)); this._metadata = new ClientMetadata(name, version); @@ -209,6 +209,7 @@ private async Task> EvaluateFlag( .Concat(OpenFeature.Instance.GetHooks()) .Concat(this._hooks) .Concat(options?.Hooks ?? Enumerable.Empty()) + .Concat(this._featureProvider.GetProviderHooks()) .ToList() .AsReadOnly(); diff --git a/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs new file mode 100644 index 00000000..3cf4bb5d --- /dev/null +++ b/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs @@ -0,0 +1,13 @@ +namespace OpenFeature.SDK.Tests +{ + public class ClearOpenFeatureInstanceFixture + { + // Make sure the singleton is cleared between tests + public ClearOpenFeatureInstanceFixture() + { + OpenFeature.Instance.SetContext(null); + OpenFeature.Instance.ClearHooks(); + OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); + } + } +} diff --git a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs index 0a48205d..b92ccb6a 100644 --- a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs @@ -9,7 +9,7 @@ namespace OpenFeature.SDK.Tests { - public class FeatureProviderTests + public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { [Fact] [Specification("2.1", "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")] @@ -67,7 +67,7 @@ public async Task Provider_Must_ErrorType() var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); - var providerMock = new Mock(); + var providerMock = new Mock(MockBehavior.Strict); providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs index 6319aa10..f10e2d2c 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs @@ -14,7 +14,7 @@ namespace OpenFeature.SDK.Tests { - public class OpenFeatureClientTests + public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture { [Fact] [Specification("1.2.1", "The client MUST provide a method to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] @@ -22,9 +22,9 @@ public void OpenFeatureClient_Should_Allow_Hooks() { var fixture = new Fixture(); var clientName = fixture.Create(); - var hook1 = new Mock().Object; - var hook2 = new Mock().Object; - var hook3 = new Mock().Object; + var hook1 = new Mock(MockBehavior.Strict).Object; + var hook2 = new Mock(MockBehavior.Strict).Object; + var hook3 = new Mock(MockBehavior.Strict).Object; var client = OpenFeature.Instance.GetClient(clientName); @@ -160,8 +160,8 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var mockedFeatureProvider = new Mock(); - var mockedLogger = new Mock>(); + var mockedFeatureProvider = new Mock(MockBehavior.Strict); + var mockedLogger = new Mock>(MockBehavior.Default); // This will fail to case a String to TestStructure mockedFeatureProvider @@ -169,6 +169,8 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc .ReturnsAsync(new ResolutionDetails(flagName, "Mismatch")); mockedFeatureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); + mockedFeatureProvider.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); OpenFeature.Instance.SetProvider(mockedFeatureProvider.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion, mockedLogger.Object); @@ -198,12 +200,14 @@ public async Task Should_Resolve_BooleanValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(); + var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveBooleanValue(flagName, defaultValue, It.IsAny(), null)) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); + featureProviderMock.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -222,12 +226,14 @@ public async Task Should_Resolve_StringValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(); + var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveStringValue(flagName, defaultValue, It.IsAny(), null)) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); + featureProviderMock.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -238,7 +244,7 @@ public async Task Should_Resolve_StringValue() } [Fact] - public async Task Should_Resolve_NumberValue() + public async Task Should_Resolve_IntegerValue() { var fixture = new Fixture(); var clientName = fixture.Create(); @@ -246,12 +252,14 @@ public async Task Should_Resolve_NumberValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(); + var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny(), null)) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); + featureProviderMock.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -261,6 +269,32 @@ public async Task Should_Resolve_NumberValue() featureProviderMock.Verify(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny(), null), Times.Once); } + [Fact] + public async Task Should_Resolve_DoubleValue() + { + var fixture = new Fixture(); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + + var featureProviderMock = new Mock(MockBehavior.Strict); + featureProviderMock + .Setup(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny(), null)) + .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.Setup(x => x.GetMetadata()) + .Returns(new Metadata(fixture.Create())); + featureProviderMock.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); + + OpenFeature.Instance.SetProvider(featureProviderMock.Object); + var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + + (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); + + featureProviderMock.Verify(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + } + [Fact] public async Task Should_Resolve_StructureValue() { @@ -270,12 +304,14 @@ public async Task Should_Resolve_StructureValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(); + var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null)) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); + featureProviderMock.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -294,12 +330,14 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(); + var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null)) .Throws(new FeatureProviderException(ErrorType.ParseError)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); + featureProviderMock.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs index da75ee61..baec1384 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs @@ -11,11 +11,12 @@ namespace OpenFeature.SDK.Tests { - public class OpenFeatureHookTests + public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture { [Fact] [Specification("1.5.1", "The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")] - [Specification("4.4.2", "Hooks MUST be evaluated in the following order: - before: API, Client, Invocation - after: Invocation, Client, API - error (if applicable): Invocation, Client, API - finally: Invocation, Client, API")] + [Specification("2.10", "The provider interface MUST define a provider hook mechanism which can be optionally implemented in order to add hook instances to the evaluation life-cycle.")] + [Specification("4.4.2", "Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API")] public async Task Hooks_Should_Be_Called_In_Order() { var fixture = new Fixture(); @@ -23,57 +24,102 @@ public async Task Hooks_Should_Be_Called_In_Order() var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var clientHook = new Mock(); - var invocationHook = new Mock(); + var apiHook = new Mock(MockBehavior.Strict); + var clientHook = new Mock(MockBehavior.Strict); + var invocationHook = new Mock(MockBehavior.Strict); + var providerHook = new Mock(MockBehavior.Strict); var sequence = new MockSequence(); - invocationHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), - It.IsAny>())) + apiHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), + It.IsAny>())) .ReturnsAsync(new EvaluationContext()); clientHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), + It.IsAny>())) + .ReturnsAsync(new EvaluationContext()); + + invocationHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) .ReturnsAsync(new EvaluationContext()); + providerHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), + It.IsAny>())) + .ReturnsAsync(new EvaluationContext()); + + providerHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), + It.IsAny>(), + It.IsAny>())).Returns(Task.CompletedTask); + invocationHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), It.IsAny>(), - It.IsAny>())); + It.IsAny>())).Returns(Task.CompletedTask); clientHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), It.IsAny>(), - It.IsAny>())); + It.IsAny>())).Returns(Task.CompletedTask); + + apiHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), + It.IsAny>(), + It.IsAny>())).Returns(Task.CompletedTask); + + providerHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), + It.IsAny>())).Returns(Task.CompletedTask); invocationHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), - It.IsAny>())); + It.IsAny>())).Returns(Task.CompletedTask); clientHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), - It.IsAny>())); + It.IsAny>())).Returns(Task.CompletedTask); - OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); + apiHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), + It.IsAny>())).Returns(Task.CompletedTask); + + var testProvider = new TestProvider(); + testProvider.AddHook(providerHook.Object); + OpenFeature.Instance.AddHooks(apiHook.Object); + OpenFeature.Instance.SetProvider(testProvider); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); client.AddHooks(clientHook.Object); await client.GetBooleanValue(flagName, defaultValue, new EvaluationContext(), new FlagEvaluationOptions(invocationHook.Object, new Dictionary())); - invocationHook.Verify(x => x.Before( + apiHook.Verify(x => x.Before( It.IsAny>(), It.IsAny>()), Times.Once); clientHook.Verify(x => x.Before( It.IsAny>(), It.IsAny>()), Times.Once); + invocationHook.Verify(x => x.Before( + It.IsAny>(), It.IsAny>()), Times.Once); + + providerHook.Verify(x => x.Before( + It.IsAny>(), It.IsAny>()), Times.Once); + + providerHook.Verify(x => x.After( + It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); + invocationHook.Verify(x => x.After( It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); clientHook.Verify(x => x.After( It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); + apiHook.Verify(x => x.After( + It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); + + providerHook.Verify(x => x.Finally( + It.IsAny>(), It.IsAny>()), Times.Once); + invocationHook.Verify(x => x.Finally( It.IsAny>(), It.IsAny>()), Times.Once); clientHook.Verify(x => x.Finally( It.IsAny>(), It.IsAny>()), Times.Once); + + apiHook.Verify(x => x.Finally( + It.IsAny>(), It.IsAny>()), Times.Once); } [Fact] @@ -122,8 +168,8 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() { var evaluationContext = new EvaluationContext { ["test"] = "test" }; - var hook1 = new Mock(); - var hook2 = new Mock(); + var hook1 = new Mock(MockBehavior.Strict); + var hook2 = new Mock(MockBehavior.Strict); var hookContext = new HookContext("test", false, FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName), evaluationContext); @@ -182,14 +228,17 @@ public async Task Hook_Should_Return_No_Errors() [Specification("4.5.3", "The hook MUST NOT alter the `hook hints` structure.")] public async Task Hook_Should_Execute_In_Correct_Order() { - var featureProvider = new Mock(); - var hook = new Mock(); + var featureProvider = new Mock(MockBehavior.Strict); + var hook = new Mock(MockBehavior.Strict); var sequence = new MockSequence(); featureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); + featureProvider.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); + hook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) .ReturnsAsync(new EvaluationContext()); @@ -217,41 +266,50 @@ public async Task Hook_Should_Execute_In_Correct_Order() } [Fact] - [Specification("4.4.1", "The API, Client and invocation MUST have a method for registering hooks which accepts `flag evaluation options`")] + [Specification("4.4.1", "The API, Client, Provider, and invocation MUST have a method for registering hooks.")] public async Task Register_Hooks_Should_Be_Available_At_All_Levels() { - var hook1 = new Mock(); - var hook2 = new Mock(); - var hook3 = new Mock(); + var hook1 = new Mock(MockBehavior.Strict); + var hook2 = new Mock(MockBehavior.Strict); + var hook3 = new Mock(MockBehavior.Strict); + var hook4 = new Mock(MockBehavior.Strict); + var testProvider = new TestProvider(); + testProvider.AddHook(hook4.Object); OpenFeature.Instance.AddHooks(hook1.Object); + OpenFeature.Instance.SetProvider(testProvider); var client = OpenFeature.Instance.GetClient(); client.AddHooks(hook2.Object); await client.GetBooleanValue("test", false, null, new FlagEvaluationOptions(hook3.Object, new Dictionary())); - client.ClearHooks(); + OpenFeature.Instance.GetHooks().Count.Should().Be(1); + client.GetHooks().Count.Should().Be(1); + testProvider.GetProviderHooks().Count.Should().Be(1); } [Fact] [Specification("4.4.3", "If a `finally` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `finally` hooks.")] public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() { - var featureProvider = new Mock(); - var hook1 = new Mock(); - var hook2 = new Mock(); + var featureProvider = new Mock(MockBehavior.Strict); + var hook1 = new Mock(MockBehavior.Strict); + var hook2 = new Mock(MockBehavior.Strict); var sequence = new MockSequence(); featureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); + featureProvider.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); + hook1.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), It.IsAny>())) + x.Before(It.IsAny>(), null)) .ReturnsAsync(new EvaluationContext()); hook2.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), It.IsAny>())) + x.Before(It.IsAny>(), null)) .ReturnsAsync(new EvaluationContext()); featureProvider.InSequence(sequence) @@ -259,76 +317,83 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() null)) .ReturnsAsync(new ResolutionDetails("test", false)); - hook1.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), It.IsAny>())); - hook2.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), It.IsAny>())); + It.IsAny>(), null)) + .Returns(Task.CompletedTask); - hook1.Setup(x => - x.Finally(It.IsAny>(), It.IsAny>())) - .Throws(new Exception()); + hook1.InSequence(sequence).Setup(x => x.After(It.IsAny>(), + It.IsAny>(), null)) + .Returns(Task.CompletedTask); hook2.InSequence(sequence).Setup(x => - x.Finally(It.IsAny>(), It.IsAny>())); + x.Finally(It.IsAny>(), null)) + .Returns(Task.CompletedTask); + + hook1.InSequence(sequence).Setup(x => + x.Finally(It.IsAny>(), null)) + .Throws(new Exception()); OpenFeature.Instance.SetProvider(featureProvider.Object); var client = OpenFeature.Instance.GetClient(); client.AddHooks(new[] { hook1.Object, hook2.Object }); + client.GetHooks().Count.Should().Be(2); await client.GetBooleanValue("test", false); - hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook1.Verify(x => x.After(It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); - hook1.Verify(x => x.Finally(It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.After(It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.Finally(It.IsAny>(), It.IsAny>()), Times.Once); + hook1.Verify(x => x.Before(It.IsAny>(), null), Times.Once); + hook2.Verify(x => x.Before(It.IsAny>(), null), Times.Once); featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Once); + hook2.Verify(x => x.After(It.IsAny>(), It.IsAny>(), null), Times.Once); + hook1.Verify(x => x.After(It.IsAny>(), It.IsAny>(), null), Times.Once); + hook2.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); + hook1.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); } [Fact] - [Specification("4.4.4", "If a `finally` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `finally` hooks.")] + [Specification("4.4.4", "If an `error` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `error` hooks.")] public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() { - var featureProvider = new Mock(); - var hook1 = new Mock(); - var hook2 = new Mock(); + var featureProvider1 = new Mock(MockBehavior.Strict); + var hook1 = new Mock(MockBehavior.Strict); + var hook2 = new Mock(MockBehavior.Strict); var sequence = new MockSequence(); - featureProvider.Setup(x => x.GetMetadata()) + featureProvider1.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); + featureProvider1.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); hook1.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), It.IsAny>())) + x.Before(It.IsAny>(), null)) .ReturnsAsync(new EvaluationContext()); hook2.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), It.IsAny>())) + x.Before(It.IsAny>(), null)) .ReturnsAsync(new EvaluationContext()); - featureProvider.InSequence(sequence) + featureProvider1.InSequence(sequence) .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null)) .Throws(new Exception()); - hook1.InSequence(sequence).Setup(x => + hook2.InSequence(sequence).Setup(x => x.Error(It.IsAny>(), It.IsAny(), null)) - .ThrowsAsync(new Exception()); + .Returns(Task.CompletedTask); - hook2.InSequence(sequence).Setup(x => - x.Error(It.IsAny>(), It.IsAny(), null)); + hook1.InSequence(sequence).Setup(x => + x.Error(It.IsAny>(), It.IsAny(), null)) + .Returns(Task.CompletedTask); - OpenFeature.Instance.SetProvider(featureProvider.Object); + OpenFeature.Instance.SetProvider(featureProvider1.Object); var client = OpenFeature.Instance.GetClient(); client.AddHooks(new[] { hook1.Object, hook2.Object }); await client.GetBooleanValue("test", false); - hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); + hook1.Verify(x => x.Before(It.IsAny>(), null), Times.Once); + hook2.Verify(x => x.Before(It.IsAny>(), null), Times.Once); hook1.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); - hook2.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); hook2.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); } @@ -336,14 +401,16 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() [Specification("4.4.6", "If an error occurs during the evaluation of `before` or `after` hooks, any remaining hooks in the `before` or `after` stages MUST NOT be invoked.")] public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_Any_Remaining_Hooks() { - var featureProvider = new Mock(); - var hook1 = new Mock(); - var hook2 = new Mock(); + var featureProvider = new Mock(MockBehavior.Strict); + var hook1 = new Mock(MockBehavior.Strict); + var hook2 = new Mock(MockBehavior.Strict); var sequence = new MockSequence(); featureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); + featureProvider.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); hook1.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) @@ -371,29 +438,35 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ [Specification("4.5.1", "`Flag evaluation options` MAY contain `hook hints`, a map of data to be provided to hook invocations.")] public async Task Hook_Hints_May_Be_Optional() { - var featureProvider = new Mock(); - var hook = new Mock(); + var featureProvider = new Mock(MockBehavior.Strict); + var hook = new Mock(MockBehavior.Strict); var defaultEmptyHookHints = new Dictionary(); var flagOptions = new FlagEvaluationOptions(hook.Object); + EvaluationContext evaluationContext = null; var sequence = new MockSequence(); featureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); + featureProvider.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); + hook.InSequence(sequence) .Setup(x => x.Before(It.IsAny>(), defaultEmptyHookHints)) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(evaluationContext); featureProvider.InSequence(sequence) .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), flagOptions)) .ReturnsAsync(new ResolutionDetails("test", false)); hook.InSequence(sequence) - .Setup(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints)); + .Setup(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints)) + .Returns(Task.CompletedTask); hook.InSequence(sequence) - .Setup(x => x.Finally(It.IsAny>(), defaultEmptyHookHints)); + .Setup(x => x.Finally(It.IsAny>(), defaultEmptyHookHints)) + .Returns(Task.CompletedTask); OpenFeature.Instance.SetProvider(featureProvider.Object); var client = OpenFeature.Instance.GetClient(); @@ -410,8 +483,8 @@ public async Task Hook_Hints_May_Be_Optional() [Specification("4.4.7", "If an error occurs in the `before` hooks, the default value MUST be returned.")] public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() { - var featureProvider = new Mock(); - var hook = new Mock(); + var featureProvider = new Mock(MockBehavior.Strict); + var hook = new Mock(MockBehavior.Strict); var exceptionToThrow = new Exception("Fails during default"); var sequence = new MockSequence(); @@ -424,10 +497,10 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() .ThrowsAsync(exceptionToThrow); hook.InSequence(sequence) - .Setup(x => x.Error(It.IsAny>(), It.IsAny(), null)); + .Setup(x => x.Error(It.IsAny>(), It.IsAny(), null)).Returns(Task.CompletedTask); hook.InSequence(sequence) - .Setup(x => x.Finally(It.IsAny>(), null)); + .Setup(x => x.Finally(It.IsAny>(), null)).Returns(Task.CompletedTask); var client = OpenFeature.Instance.GetClient(); client.AddHooks(hook.Object); diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureTests.cs index 5c7cb768..03144afa 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureTests.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using AutoFixture; using FluentAssertions; -using Microsoft.Extensions.Logging; using Moq; using OpenFeature.SDK.Constant; using OpenFeature.SDK.Model; @@ -12,7 +8,7 @@ namespace OpenFeature.SDK.Tests { - public class OpenFeatureTests + public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { [Fact] [Specification("1.1.1", "The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")] @@ -29,10 +25,10 @@ public void OpenFeature_Should_Be_Singleton() public void OpenFeature_Should_Add_Hooks() { var openFeature = OpenFeature.Instance; - var hook1 = new Mock().Object; - var hook2 = new Mock().Object; - var hook3 = new Mock().Object; - var hook4 = new Mock().Object; + var hook1 = new Mock(MockBehavior.Strict).Object; + var hook2 = new Mock(MockBehavior.Strict).Object; + var hook3 = new Mock(MockBehavior.Strict).Object; + var hook4 = new Mock(MockBehavior.Strict).Object; openFeature.ClearHooks(); diff --git a/test/OpenFeature.SDK.Tests/TestImplementations.cs b/test/OpenFeature.SDK.Tests/TestImplementations.cs index 0898100f..0b2d29cd 100644 --- a/test/OpenFeature.SDK.Tests/TestImplementations.cs +++ b/test/OpenFeature.SDK.Tests/TestImplementations.cs @@ -37,48 +37,54 @@ public override Task Finally(HookContext context, IReadOnlyDictionary _hooks = new List(); + public static string Name => "test-provider"; - public Metadata GetMetadata() + public void AddHook(Hook hook) => this._hooks.Add(hook); + + public override IReadOnlyList GetProviderHooks() => this._hooks.AsReadOnly(); + + public override Metadata GetMetadata() { return new Metadata(Name); } - public Task> ResolveBooleanValue(string flagKey, bool defaultValue, + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { - throw new NotImplementedException(); + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public Task> ResolveStringValue(string flagKey, string defaultValue, + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { - throw new NotImplementedException(); + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public Task> ResolveIntegerValue(string flagKey, int defaultValue, + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { - throw new NotImplementedException(); + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public Task> ResolveDoubleValue(string flagKey, double defaultValue, + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { - throw new NotImplementedException(); + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public Task> ResolveStructureValue(string flagKey, T defaultValue, + public override Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) { - throw new NotImplementedException(); + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } } } From 10329cf061f92a6773e5ed61707007a808794e73 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 5 Aug 2022 21:56:08 +1000 Subject: [PATCH 011/316] Chore: Remove IHook inteface and just use Hook abstract class There is no reason to have the internal IHook interface anymore. C#8 included default implementations on interfaces but since we are on netstandard2.0 we only have access to C#7.3 Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- src/OpenFeature.SDK/Hook.cs | 10 +--------- src/OpenFeature.SDK/OpenFeatureClient.cs | 8 ++++---- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/OpenFeature.SDK/Hook.cs b/src/OpenFeature.SDK/Hook.cs index 866351e7..7159cae0 100644 --- a/src/OpenFeature.SDK/Hook.cs +++ b/src/OpenFeature.SDK/Hook.cs @@ -5,14 +5,6 @@ namespace OpenFeature.SDK { - internal interface IHook - { - Task Before(HookContext context, IReadOnlyDictionary hints = null); - Task After(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null); - Task Error(HookContext context, Exception error, IReadOnlyDictionary hints = null); - Task Finally(HookContext context, IReadOnlyDictionary hints = null); - } - /// /// 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. @@ -27,7 +19,7 @@ internal interface IHook /// /// /// Hook Specification - public abstract class Hook : IHook + public abstract class Hook { /// /// Called immediately before flag evaluation. diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs index 0a54df3d..a3d02bf2 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeature.SDK/OpenFeatureClient.cs @@ -260,7 +260,7 @@ private async Task> EvaluateFlag( return evaluation; } - private async Task TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, + private async Task TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, FlagEvaluationOptions options) { foreach (var hook in hooks) @@ -278,7 +278,7 @@ private async Task TriggerBeforeHooks(IReadOnlyList hooks, HookContext } } - private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext context, + private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext context, FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions options) { foreach (var hook in hooks) @@ -287,7 +287,7 @@ private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext< } } - private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext context, Exception exception, + private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext context, Exception exception, FlagEvaluationOptions options) { foreach (var hook in hooks) @@ -303,7 +303,7 @@ private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext< } } - private async Task TriggerFinallyHooks(IReadOnlyList hooks, HookContext context, + private async Task TriggerFinallyHooks(IReadOnlyList hooks, HookContext context, FlagEvaluationOptions options) { foreach (var hook in hooks) From 7eb400cf12a12f6cfdcff782cf662726763220d8 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sat, 6 Aug 2022 09:45:22 +1000 Subject: [PATCH 012/316] Chore: Prove able to fetch all fields in EvaluationContext The current implementation of the EvaluationContext is based on a dictionary. This is a list of key-value pairs. Able to use Linq based methods to iterate, fetch, manipulate and aggregate Spec 3.1.3 The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs. Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .../OpenFeatureEvaluationContextTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs index 5973aa09..327a8003 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs @@ -88,5 +88,29 @@ public void When_Duplicate_Key_Throw_Unique_Constraint() context.Add("key", "overriden_value")); exception.Message.Should().StartWith("An item with the same key has already been added."); } + + [Fact] + [Specification("3.1.3", "The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")] + public void Should_Be_Able_To_Get_All_Values() + { + var context = new EvaluationContext + { + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" }, + { "key4", "value4" }, + { "key5", "value5" } + }; + + // Iterate over key value pairs and check consistency + var count = 0; + foreach (var keyValue in context) + { + context[keyValue.Key].Should().Be(keyValue.Value); + count++; + } + + context.Count.Should().Be(count); + } } } From 310482069fab88a46e71bab475f0e4c7012e2b7e Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Wed, 10 Aug 2022 21:59:41 +1000 Subject: [PATCH 013/316] chore: Add openssf badge Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9458c10b..84d9809f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)]() [![codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) [![nuget](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://bestpractices.coreinfrastructure.org/projects/6250) OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 300233323ebfdb4a64cf9efa3b63c4c7f93b7fb5 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Wed, 10 Aug 2022 22:43:07 +1000 Subject: [PATCH 014/316] chore: update readme - Include examples of implmenting hooks and providers - Include CONTRIBUTING.md Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- CONTRIBUTING.md | 107 ++++++++++++++++++++++++++++++++++++++ OpenFeature.SDK.sln | 2 + README.md | 122 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..239e38a9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,107 @@ +# Contributing to the OpenFeature project + +Our community meetings are held fortnightly on Thursdays at 4pm. Check the [OpenFeature community calendar](https://calendar.google.com/calendar/u/0?cid=MHVhN2kxaGl2NWRoMThiMjd0b2FoNjM2NDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) for specific dates and for the Zoom meeting links. + +## Development + +You can contribute to this project from a Windows, macOS or Linux machine. + +On all platforms, the minimum requirements are: + +* Git client and command line tools. +* .netstandard 2.0 or higher capable dotnet sdk (.Net Framework 4.6.2 or higher/.Net Core 3 or higher). + +### Linux or MacOS + +* Jetbrains Rider 2022.2+ or Visual Studio 2022+ for Mac or Visual Studio Code + +### Windows + +* Jetbrains Rider 2022.2+ or Visual Studio 2022+ or Visual Studio Code +* .NET Framework 4.6.2+ + +## Pull Request + +All contributions to the OpenFeature project are welcome via GitHub pull requests. + +To create a new PR, you will need to first fork the GitHub repository and clone upstream. + +```bash +git clone https://github.com/open-feature/dotnet-sdk.git openfeature-dotnet-sdk +``` + +Navigate to the repository folder +```bash +cd openfeature-dotnet-sdk +``` + +Add your fork as an origin +```bash +git remote add fork https://github.com/YOUR_GITHUB_USERNAME/dotnet-sdk.git +``` + +Makes sure your development environment is all setup by building and testing +```bash +dotnet build +dotnet test +``` + +To start working on a new feature or bugfix, create a new branch and start working on it. + +```bash +git checkout -b feat/NAME_OF_FEATURE +# Make your changes +git commit +git push fork feat/NAME_OF_FEATURE +``` + +Open a pull request against the main dotnet-sdk repository. + +### How to Receive Comments + +* If the PR is not ready for review, please mark it as + [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). +* Make sure all required CI checks are clear. +* Submit small, focused PRs addressing a single concern/issue. +* Make sure the PR title reflects the contribution. +* Write a summary that helps understand the change. +* Include usage examples in the summary, where applicable. + +### How to Get PRs Merged + +A PR is considered to be **ready to merge** when: + +* Major feedbacks are resolved. +* It has been open for review for at least one working day. This gives people + reasonable time to review. +* Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. +* Urgent fix can take exception as long as it has been actively communicated. + +Any Maintainer can merge the PR once it is **ready to merge**. Note, that some +PRs may not be merged immediately if the repo is in the process of a release and +the maintainers decided to defer the PR to the next release train. + +If a PR has been stuck (e.g. there are lots of debates and people couldn't agree +on each other), the owner should try to get people aligned by: + +* Consolidating the perspectives and putting a summary in the PR. It is + recommended to add a link into the PR description, which points to a comment + with a summary in the PR conversation. +* Tagging subdomain experts (by looking at the change history) in the PR asking + for suggestion. +* Reaching out to more people on the [CNCF OpenFeature Slack channel](https://cloud-native.slack.com/archives/C0344AANLA1). +* Stepping back to see if it makes sense to narrow down the scope of the PR or + split it up. +* If none of the above worked and the PR has been stuck for more than 2 weeks, + the owner should bring it to the OpenFeatures [meeting](README.md#contributing). + +## Design Choices + +As with other OpenFeature SDKs, dotnet-sdk follows the +[openfeature-specification](https://github.com/open-feature/spec). + +## Style Guide + +This project includes a [`.editorconfig`](./.editorconfig) file which is +supported by all the IDEs/editor mentioned above. It works with the IDE/editor +only and does not affect the actual build of the project. diff --git a/OpenFeature.SDK.sln b/OpenFeature.SDK.sln index cc483c05..f2f55aac 100644 --- a/OpenFeature.SDK.sln +++ b/OpenFeature.SDK.sln @@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60 .github\workflows\linux-ci.yml = .github\workflows\linux-ci.yml .github\workflows\release-package.yml = .github\workflows\release-package.yml .github\workflows\windows-ci.yml = .github\workflows\windows-ci.yml + README.md = README.md + CONTRIBUTING.md = CONTRIBUTING.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C97E9975-E10A-4817-AE2C-4DD69C3C02D4}" diff --git a/README.md b/README.md index 9458c10b..ebb4f47e 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,130 @@ The packages will aim to support all current .NET versions. Refer to the current | ----------- | ----------- | | TBA | TBA | -## Basic Usage +## Getting Started + +### Basic Usage ```csharp using OpenFeature.SDK; +// Sets the provider used by the client OpenFeature.Instance.SetProvider(new NoOpProvider()); +// Gets a instance of the feature flag client var client = OpenFeature.Instance.GetClient(); - +// Evaluation the `my-feature` feature flag var isEnabled = await client.GetBooleanValue("my-feature", false); ``` -## Contributors +### Provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in an existing contrib repository available under the OpenFeature organization. Finally, you’ll then need to write the provider itself. In most languages, this can be accomplished by implementing the provider interface exported by the OpenFeature SDK. + +Example of implementing a feature flag provider + +```csharp +using OpenFeature.SDK; +using OpenFeature.SDK.Model; + +public class MyFeatureProvider : FeatureProvider +{ + public static string Name => "My Feature Provider"; + + public Metadata GetMetadata() + { + return new Metadata(Name); + } + + public Task> ResolveBooleanValue(string flagKey, bool defaultValue, + EvaluationContext context = null, + FlagEvaluationOptions config = null) + { + // code to resolve boolean details + } + + public Task> ResolveStringValue(string flagKey, string defaultValue, + EvaluationContext context = null, + FlagEvaluationOptions config = null) + { + // code to resolve string details + } + + public Task> ResolveIntegerValue(string flagKey, int defaultValue, + EvaluationContext context = null, + FlagEvaluationOptions config = null) + { + // code to resolve integer details + } + + public Task> ResolveDoubleValue(string flagKey, double defaultValue, + EvaluationContext context = null, + FlagEvaluationOptions config = null) + { + // code to resolve integer details + } + + public Task> ResolveStructureValue(string flagKey, T defaultValue, + EvaluationContext context = null, + FlagEvaluationOptions config = null) + { + // code to resolve object details + } +} +``` + +### Hook + +Hooks are a mechanism that allow for the addition of arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validation of the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking. + +Example of adding a hook + +```csharp +// add a hook globally, to run on all evaluations +openFeature.AddHooks(new ExampleGlobalHook()); + +// add a hook on this client, to run on all evaluations made by this client +var client = OpenFeature.Instance.GetClient(); +client.AddHooks(new ExampleClientHook()); + +// add a hook for this evaluation only +var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); +``` + +Example of implementing a hook + +```csharp +public class MyHook : Hook +{ + public Task Before(HookContext context, + IReadOnlyDictionary hints = null) + { + // code to run before flag evaluation + } + + public virtual Task After(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null) + { + // code to run after successful flag evaluation + } + + public virtual Task Error(HookContext context, Exception error, + IReadOnlyDictionary hints = null) + { + // code to run if there's an error during before hooks or during flag evaluation + } + + public virtual Task Finally(HookContext context, IReadOnlyDictionary hints = null) + { + // code to run after all other stages, regardless of success/failure + } +} +``` + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to the OpenFeature project. + +Our community meetings are held fortnightly on Thursdays at 4pm. Check the [OpenFeature community calendar](https://calendar.google.com/calendar/u/0?cid=MHVhN2kxaGl2NWRoMThiMjd0b2FoNjM2NDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) for specific dates and for the Zoom meeting links. Thanks so much for your contributions to the OpenFeature project. @@ -37,3 +149,7 @@ Thanks so much for your contributions to the OpenFeature project. Made with [contrib.rocks](https://contrib.rocks). + +## License + +Apache License 2.0 From 9f3f879b2fabf705ff2577d7ae5d0a737aa61827 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 10 Aug 2022 16:10:02 -0400 Subject: [PATCH 015/316] feat: update ctx merge order, add client ctx Signed-off-by: Todd Baert --- .gitignore | 7 ++ .vscode/launch.json | 26 +++++++ .vscode/tasks.json | 41 +++++++++++ src/OpenFeature.SDK/OpenFeature.cs | 5 +- src/OpenFeature.SDK/OpenFeatureClient.cs | 13 +++- .../OpenFeatureHookTests.cs | 69 ++++++++++++++++++- 6 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index 8b91dfe1..0bc3c4de 100644 --- a/.gitignore +++ b/.gitignore @@ -341,3 +341,10 @@ ASALocalRun/ /.sonarqube /src/LastMajorVersionBinaries + +# vscode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..4d46e206 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/test/OpenFeature.Tests/bin/Debug/net6.0/OpenFeature.Tests.dll", + "args": [], + "cwd": "${workspaceFolder}/test/OpenFeature.Tests", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..b752a031 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/test/OpenFeature.Tests/OpenFeature.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/test/OpenFeature.Tests/OpenFeature.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/test/OpenFeature.Tests/OpenFeature.Tests.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/OpenFeature.SDK/OpenFeature.cs b/src/OpenFeature.SDK/OpenFeature.cs index 8099299b..31079cd0 100644 --- a/src/OpenFeature.SDK/OpenFeature.cs +++ b/src/OpenFeature.SDK/OpenFeature.cs @@ -50,9 +50,10 @@ private OpenFeature() { } /// 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) => - new FeatureClient(this._featureProvider, name, version, logger); + public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null, EvaluationContext context = null) => + new FeatureClient(this._featureProvider, name, version, logger, context); /// /// Appends list of hooks to global hooks list diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs index e3e2e56e..f2a3bb02 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeature.SDK/OpenFeatureClient.cs @@ -20,6 +20,13 @@ public sealed class FeatureClient : IFeatureClient private readonly FeatureProvider _featureProvider; private readonly List _hooks = new List(); private readonly ILogger _logger; + private readonly EvaluationContext _evaluationContext; + + /// + /// Gets the client + /// + /// + public EvaluationContext GetContext() => this._evaluationContext; /// /// Initializes a new instance of the class. @@ -28,12 +35,14 @@ public sealed class FeatureClient : IFeatureClient /// Name of client /// Version of client /// Logger used by client + /// Context given to this client /// Throws if any of the required parameters are null - public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null) + public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null, EvaluationContext context = null) { this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider)); this._metadata = new ClientMetadata(name, version); this._logger = logger ?? new Logger(new NullLoggerFactory()); + this._evaluationContext = context ?? new EvaluationContext(); } /// @@ -202,7 +211,9 @@ private async Task> EvaluateFlag( context = new EvaluationContext(); } + // merge api, client, and invocation context. var evaluationContext = OpenFeature.Instance.GetContext(); + evaluationContext.Merge(this.GetContext()); evaluationContext.Merge(context); var allHooks = new List() diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs index baec1384..95de0f62 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs @@ -164,7 +164,6 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() [Fact] [Specification("4.1.4", "The evaluation context MUST be mutable only within the `before` hook.")] [Specification("4.3.3", "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")] - [Specification("4.3.4", "When `before` hooks have finished executing, any resulting `evaluation context` MUST be merged with the invocation `evaluation context` with the invocation `evaluation context` taking precedence in the case of any conflicts.")] public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() { var evaluationContext = new EvaluationContext { ["test"] = "test" }; @@ -189,6 +188,74 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.Get("test") == "test"), It.IsAny>()), Times.Once); } + [Fact] + [Specification("4.3.4", "When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context in the following order: before-hook (highest precedence), invocation, client, api (lowest precedence).")] + public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() + { + var propGlobal = "4.3.4global"; + var propGlobalToOverwrite = "4.3.4globalToOverwrite"; + + var propClient = "4.3.4client"; + var propClientToOverwrite = "4.3.4clientToOverwrite"; + + var propInvocation = "4.3.4invocation"; + var propInvocationToOverwrite = "4.3.4invocationToOverwrite"; + + var propHook = "4.3.4hook"; + + // setup a cascade of overwriting properties + OpenFeature.Instance.SetContext(new EvaluationContext { + [propGlobal] = true, + [propGlobalToOverwrite] = false + }); + var clientContext = new EvaluationContext { + [propClient] = true, + [propGlobalToOverwrite] = true, + [propClientToOverwrite] = false + }; + var invocationContext = new EvaluationContext { + [propInvocation] = true, + [propClientToOverwrite] = true, + [propInvocationToOverwrite] = false, + }; + var hookContext = new EvaluationContext { + [propHook] = true, + [propInvocationToOverwrite] = true, + }; + + var provider = new Mock(MockBehavior.Strict); + + provider.Setup(x => x.GetMetadata()) + .Returns(new Metadata(null)); + + provider.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); + + provider.Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null)) + .ReturnsAsync(new ResolutionDetails("test", true)); + + OpenFeature.Instance.SetProvider(provider.Object); + + var hook = new Mock(MockBehavior.Strict); + hook.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(hookContext); + + + var client = OpenFeature.Instance.GetClient("test", "1.0.0", null, clientContext); + await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(new[] { hook.Object }, new Dictionary())); + + // after proper merging, all properties should equal true + provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => + y.Get(propGlobal) + && y.Get(propClient) + && y.Get(propGlobalToOverwrite) + && y.Get(propInvocation) + && y.Get(propClientToOverwrite) + && y.Get(propHook) + && y.Get(propInvocationToOverwrite) + ), It.IsAny()), Times.Once); + } + [Fact] [Specification("4.2.1", "`hook hints` MUST be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`..")] [Specification("4.2.2.1", "Condition: `Hook hints` MUST be immutable.")] From 0c26965535aa2d7b7fd0bdcc44638a7cf40f021f Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 10 Aug 2022 16:18:16 -0400 Subject: [PATCH 016/316] Fix formatting Signed-off-by: Todd Baert --- .../OpenFeatureHookTests.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs index 95de0f62..89c0d415 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs @@ -204,27 +204,31 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() var propHook = "4.3.4hook"; // setup a cascade of overwriting properties - OpenFeature.Instance.SetContext(new EvaluationContext { + OpenFeature.Instance.SetContext(new EvaluationContext + { [propGlobal] = true, [propGlobalToOverwrite] = false }); - var clientContext = new EvaluationContext { + var clientContext = new EvaluationContext + { [propClient] = true, [propGlobalToOverwrite] = true, [propClientToOverwrite] = false }; - var invocationContext = new EvaluationContext { + var invocationContext = new EvaluationContext + { [propInvocation] = true, [propClientToOverwrite] = true, [propInvocationToOverwrite] = false, }; - var hookContext = new EvaluationContext { + var hookContext = new EvaluationContext + { [propHook] = true, [propInvocationToOverwrite] = true, }; - var provider = new Mock(MockBehavior.Strict); - + var provider = new Mock(MockBehavior.Strict); + provider.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); @@ -245,7 +249,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(new[] { hook.Object }, new Dictionary())); // after proper merging, all properties should equal true - provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => + provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => y.Get(propGlobal) && y.Get(propClient) && y.Get(propGlobalToOverwrite) From e224a5330c3c02ebdf9b30fe7954bf789a8ee2f0 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 11 Aug 2022 08:01:44 +1000 Subject: [PATCH 017/316] Don't need to specify date time Avoid updates to this in the future, users can refer to calendar linke Co-authored-by: Michael Beemer Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ebb4f47e..a4ec0e4a 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,8 @@ public class MyHook : Hook See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to the OpenFeature project. -Our community meetings are held fortnightly on Thursdays at 4pm. Check the [OpenFeature community calendar](https://calendar.google.com/calendar/u/0?cid=MHVhN2kxaGl2NWRoMThiMjd0b2FoNjM2NDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) for specific dates and for the Zoom meeting links. +Our community meetings are held regularly and open to everyone. Check the [OpenFeature community calendar](https://calendar.google.com/calendar/u/0?cid=MHVhN2kxaGl2NWRoMThiMjd0b2FoNjM2NDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) for specific dates and for the Zoom meeting links. + Thanks so much for your contributions to the OpenFeature project. From 279737a98d4439ebc3ba2ce7c2ecd1e9b432ab05 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 11 Aug 2022 08:04:26 +1000 Subject: [PATCH 018/316] chore: Link to license file Remove duplicate data Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- CONTRIBUTING.md | 2 -- README.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 239e38a9..5a6427fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,5 @@ # Contributing to the OpenFeature project -Our community meetings are held fortnightly on Thursdays at 4pm. Check the [OpenFeature community calendar](https://calendar.google.com/calendar/u/0?cid=MHVhN2kxaGl2NWRoMThiMjd0b2FoNjM2NDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) for specific dates and for the Zoom meeting links. - ## Development You can contribute to this project from a Windows, macOS or Linux machine. diff --git a/README.md b/README.md index a4ec0e4a..05bbe687 100644 --- a/README.md +++ b/README.md @@ -153,4 +153,4 @@ Made with [contrib.rocks](https://contrib.rocks). ## License -Apache License 2.0 +[Apache License 2.0](LICENSE) From 6388449e86f0c15c165061d5aadc51aa5ec3179c Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 11 Aug 2022 22:00:04 +1000 Subject: [PATCH 019/316] chore: add dotnet-format to the tools Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .config/dotnet-tools.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 0bbbe55c..51aa1ce1 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "minver" ] + }, + "dotnet-format": { + "version": "5.1.250801", + "commands": [ + "dotnet-format" + ] } } } \ No newline at end of file From 00e9fa7e2b0cbc149478776d4302af729d9ab3b9 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 11 Aug 2022 10:23:15 +1000 Subject: [PATCH 020/316] bug: Correct release action - Must tell minver that tag prefix is v - Update proj name after rename of project - Remove version matrix since we don't need to run on mutliple architecture Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .github/workflows/release-package.yml | 36 ++++++++++++--------------- build/Common.prod.props | 1 + build/RELEASING.md | 7 +++++- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 96629a3d..e34e591b 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -3,34 +3,30 @@ name: Release Package on: push: tags: - - 'v[0-9]+.[0-9]+.[0-9]+' + - "v[0-9]+.[0-9]+.[0-9]+" jobs: release-package: runs-on: windows-latest - strategy: - matrix: - version: [net462,net6.0] - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Restore Tools - run: dotnet tool restore + - name: Restore Tools + run: dotnet tool restore - - name: Install dependencies - run: dotnet restore + - name: Install dependencies + run: dotnet restore - - name: Build - run: dotnet build --configuration Release --no-restore -p:Deterministic=true + - name: Build + run: dotnet build --configuration Release --no-restore -p:Deterministic=true - - name: Pack - run: dotnet pack OpenFeature.proj --configuration Release --no-build + - name: Pack + run: dotnet pack OpenFeature.SDK.proj --configuration Release --no-build -p:PackageID=OpenFeature - - name: Publish to Nuget - run: | - VERSION=$(dotnet minver -v e) - dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json + - name: Publish to Nuget + run: | + VERSION=$(dotnet minver -t v -v e) + dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json diff --git a/build/Common.prod.props b/build/Common.prod.props index aacfc0c7..fab25878 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -13,6 +13,7 @@ + v 0.1 diff --git a/build/RELEASING.md b/build/RELEASING.md index a8b28b19..2b48536a 100644 --- a/build/RELEASING.md +++ b/build/RELEASING.md @@ -4,16 +4,21 @@ Only for release managers 1. Decide on the version name to be released. e.g. 0.1.0, 0.1.1 etc 2. Tag the commit with the version number + ```shell git tag -a 0.1.0 -m "0.1.0" git push origin 0.1.0 ``` + 3. Build and pack the code + ```shell dotnet build --configuration Release --no-restore -p:Deterministic=true -dotnet pack OpenFeature.proj --configuration Release --no-build +dotnet pack OpenFeature.SDK.proj --configuration Release --no-build ``` + 4. Push up the package to nuget + ```shell dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key {API_KEY} --source https://api.nuget.org/v3/index.json ``` From afe02d62e0b1b25693515b017fd7b2250ae85409 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sun, 14 Aug 2022 23:30:51 +1000 Subject: [PATCH 021/316] feat: Use dotnet-releaser to automate releases - Update doco to reflect changes - Configure dotnet-releaser and add to tools - Update release action to use dotnet-release - Add description to package, and packageid Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .config/dotnet-tools.json | 14 ++++++------ .github/workflows/release-package.yml | 15 ++----------- .gitignore | 5 ++++- CONTRIBUTING.md | 16 ++++++++++++++ OpenFeature.SDK.sln | 1 + build/Common.prod.props | 2 ++ build/RELEASING.md | 19 ++-------------- build/dotnet-releaser.toml | 22 +++++++++++++++++++ .../OpenFeature.SDK.Tests.csproj | 4 ---- 9 files changed, 56 insertions(+), 42 deletions(-) create mode 100644 build/dotnet-releaser.toml diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 51aa1ce1..494c6898 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -2,17 +2,17 @@ "version": 1, "isRoot": true, "tools": { - "minver-cli": { - "version": "4.1.0", - "commands": [ - "minver" - ] - }, "dotnet-format": { "version": "5.1.250801", "commands": [ "dotnet-format" ] + }, + "dotnet-releaser": { + "version": "0.4.2", + "commands": [ + "dotnet-releaser" + ] } } -} \ No newline at end of file +} diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index e34e591b..a8199e32 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -17,16 +17,5 @@ jobs: - name: Restore Tools run: dotnet tool restore - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore -p:Deterministic=true - - - name: Pack - run: dotnet pack OpenFeature.SDK.proj --configuration Release --no-build -p:PackageID=OpenFeature - - - name: Publish to Nuget - run: | - VERSION=$(dotnet minver -t v -v e) - dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json + - name: Build, Tests, Pack and Publish + run: dotnet releaser run --nuget-token "${{secrets.NUGET_TOKEN}}" --github-token "${{secrets.GITHUB_TOKEN}}" build/dotnet-releaser.toml diff --git a/.gitignore b/.gitignore index 0bc3c4de..c021583e 100644 --- a/.gitignore +++ b/.gitignore @@ -347,4 +347,7 @@ ASALocalRun/ !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# dotnet releaser artifacts +/build/artifacts-dotnet-releaser diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a6427fd..ba1064a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,6 +93,22 @@ on each other), the owner should try to get people aligned by: * If none of the above worked and the PR has been stuck for more than 2 weeks, the owner should bring it to the OpenFeatures [meeting](README.md#contributing). +## Automated Changelog + +Each time a release is published the changelogs will be generated automatically using [dotnet-releaser](https://github.com/xoofx/dotnet-releaser/blob/main/doc/changelog_user_guide.md#13-categories). The tool will organise the changes based on the PR labels. + +- 🚨 Breaking Changes = `breaking-change` +- ✨ New Features = `feature` +- πŸ› Bug Fixes = `bug` +- πŸš€ Enhancements = `enhancement` +- 🧰 Maintenance = `maintenance` +- 🏭 Tests = `tests`, `test` +- πŸ›  Examples = `examples` +- πŸ“š Documentation = `documentation` +- 🌎 Accessibility = `translations` +- πŸ“¦ Dependencies = `dependencies` +- 🧰 Misc = `misc` + ## Design Choices As with other OpenFeature SDKs, dotnet-sdk follows the diff --git a/OpenFeature.SDK.sln b/OpenFeature.SDK.sln index f2f55aac..d1986dc1 100644 --- a/OpenFeature.SDK.sln +++ b/OpenFeature.SDK.sln @@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60 .github\workflows\windows-ci.yml = .github\workflows\windows-ci.yml README.md = README.md CONTRIBUTING.md = CONTRIBUTING.md + build\dotnet-releaser.toml = build\dotnet-releaser.toml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C97E9975-E10A-4817-AE2C-4DD69C3C02D4}" diff --git a/build/Common.prod.props b/build/Common.prod.props index fab25878..b440beb3 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -20,7 +20,9 @@ git https://github.com/open-feature/dotnet-sdk + OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. Feature;OpenFeature;Flags; + OpenFeature openfeature-icon.png https://openfeature.dev Apache-2.0 diff --git a/build/RELEASING.md b/build/RELEASING.md index 2b48536a..be29dbd3 100644 --- a/build/RELEASING.md +++ b/build/RELEASING.md @@ -3,22 +3,7 @@ Only for release managers 1. Decide on the version name to be released. e.g. 0.1.0, 0.1.1 etc -2. Tag the commit with the version number +2. Create tag via github ui +3. Release package action will fire on tag creation that will build, pack and publish and create the github release -```shell -git tag -a 0.1.0 -m "0.1.0" -git push origin 0.1.0 -``` -3. Build and pack the code - -```shell -dotnet build --configuration Release --no-restore -p:Deterministic=true -dotnet pack OpenFeature.SDK.proj --configuration Release --no-build -``` - -4. Push up the package to nuget - -```shell -dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key {API_KEY} --source https://api.nuget.org/v3/index.json -``` diff --git a/build/dotnet-releaser.toml b/build/dotnet-releaser.toml new file mode 100644 index 00000000..a4ebcb18 --- /dev/null +++ b/build/dotnet-releaser.toml @@ -0,0 +1,22 @@ +# configuration file for dotnet-releaser +[msbuild] +project = "../OpenFeature.SDK.sln" +configuration = "Release" +[msbuild.properties] +Deterministic = true +[github] +user = "open-feature" +repo = "dotnet-sdk" +version_prefix = "v" +[test] +enable = false +[coverage] +enable = false +[coveralls] +publish = false +[brew] +publish = false +[service] +publish = false +[changelog] +publish = true diff --git a/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj b/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj index f9765bb6..8da08d04 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj +++ b/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj @@ -12,10 +12,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - From 0594ed7e91df817dbf1bf89d1cfebab99ab47e6d Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 26 Aug 2022 07:25:55 +1000 Subject: [PATCH 022/316] Removed evaluation options from provider As per spec 0.4 remove flag evaluation option from the provider - Update FeatureProvider - Update doco Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- README.md | 15 +++----- src/OpenFeature.SDK/FeatureProvider.cs | 15 +++----- src/OpenFeature.SDK/NoOpProvider.cs | 11 +++--- src/OpenFeature.SDK/OpenFeatureClient.cs | 4 +-- .../FeatureProviderTests.cs | 14 ++++---- .../OpenFeatureClientTests.cs | 28 +++++++-------- .../OpenFeatureHookTests.cs | 34 +++++++++---------- .../TestImplementations.cs | 15 +++----- 8 files changed, 59 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index cd144c20..852163e8 100644 --- a/README.md +++ b/README.md @@ -53,36 +53,31 @@ public class MyFeatureProvider : FeatureProvider } public Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { // code to resolve boolean details } public Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { // code to resolve string details } public Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { // code to resolve integer details } public Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { // code to resolve integer details } public Task> ResolveStructureValue(string flagKey, T defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { // code to resolve object details } diff --git a/src/OpenFeature.SDK/FeatureProvider.cs b/src/OpenFeature.SDK/FeatureProvider.cs index ef767b4a..21c3aaa8 100644 --- a/src/OpenFeature.SDK/FeatureProvider.cs +++ b/src/OpenFeature.SDK/FeatureProvider.cs @@ -37,10 +37,9 @@ public abstract class FeatureProvider /// Feature flag key /// Default value /// - /// /// public abstract Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null); + EvaluationContext context = null); /// /// Resolves a string feature flag @@ -48,10 +47,9 @@ public abstract Task> ResolveBooleanValue(string flagKey /// Feature flag key /// Default value /// - /// /// public abstract Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null); + EvaluationContext context = null); /// /// Resolves a integer feature flag @@ -59,10 +57,9 @@ public abstract Task> ResolveStringValue(string flagKe /// Feature flag key /// Default value /// - /// /// public abstract Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null); + EvaluationContext context = null); /// /// Resolves a double feature flag @@ -70,10 +67,9 @@ public abstract Task> ResolveIntegerValue(string flagKey, /// Feature flag key /// Default value /// - /// /// public abstract Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null); + EvaluationContext context = null); /// /// Resolves a structured feature flag @@ -81,10 +77,9 @@ public abstract Task> ResolveDoubleValue(string flagKe /// Feature flag key /// Default value /// - /// /// Type of object /// public abstract Task> ResolveStructureValue(string flagKey, T defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null); + EvaluationContext context = null); } } diff --git a/src/OpenFeature.SDK/NoOpProvider.cs b/src/OpenFeature.SDK/NoOpProvider.cs index 0b5d621b..015c7dc5 100644 --- a/src/OpenFeature.SDK/NoOpProvider.cs +++ b/src/OpenFeature.SDK/NoOpProvider.cs @@ -13,28 +13,27 @@ public override Metadata GetMetadata() return this._metadata; } - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) + public override Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs index f2a3bb02..13ae1f28 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeature.SDK/OpenFeatureClient.cs @@ -201,7 +201,7 @@ await this.EvaluateFlag(this._featureProvider.ResolveStructureValue, FlagValueTy defaultValue, context, config); private async Task> EvaluateFlag( - Func>> resolveValueDelegate, + Func>> resolveValueDelegate, FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions options = null) { @@ -244,7 +244,7 @@ private async Task> EvaluateFlag( await this.TriggerBeforeHooks(allHooks, hookContext, options); evaluation = - (await resolveValueDelegate.Invoke(flagKey, defaultValue, hookContext.EvaluationContext, options)) + (await resolveValueDelegate.Invoke(flagKey, defaultValue, hookContext.EvaluationContext)) .ToFlagEvaluationDetails(); await this.TriggerAfterHooks(allHooksReversed, hookContext, evaluation, options); diff --git a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs index b92ccb6a..541ee5c5 100644 --- a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs @@ -21,7 +21,7 @@ public void Provider_Must_Have_Metadata() } [Fact] - [Specification("2.2", "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns a `flag resolution` structure.")] + [Specification("2.2", "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `flag resolution` structure.")] [Specification("2.3.1", "The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.")] [Specification("2.4", "In cases of normal execution, the `provider` MUST populate the `flag resolution` structure's `value` field with the resolved flag value.")] [Specification("2.5", "In cases of normal execution, the `provider` SHOULD populate the `flag resolution` structure's `variant` field with a string identifier corresponding to the returned flag value.")] @@ -69,22 +69,22 @@ public async Task Provider_Must_ErrorType() var defaultStructureValue = fixture.Create(); var providerMock = new Mock(MockBehavior.Strict); - providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny(), It.IsAny())) + providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - providerMock.Setup(x => x.ResolveIntegerValue(flagName, defaultIntegerValue, It.IsAny(), It.IsAny())) + providerMock.Setup(x => x.ResolveIntegerValue(flagName, defaultIntegerValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - providerMock.Setup(x => x.ResolveDoubleValue(flagName, defaultDoubleValue, It.IsAny(), It.IsAny())) + providerMock.Setup(x => x.ResolveDoubleValue(flagName, defaultDoubleValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - providerMock.Setup(x => x.ResolveStringValue(flagName, defaultStringValue, It.IsAny(), It.IsAny())) + providerMock.Setup(x => x.ResolveStringValue(flagName, defaultStringValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - providerMock.Setup(x => x.ResolveStructureValue(flagName, defaultStructureValue, It.IsAny(), It.IsAny())) + providerMock.Setup(x => x.ResolveStructureValue(flagName, defaultStructureValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - providerMock.Setup(x => x.ResolveStructureValue(flagName2, defaultStructureValue, It.IsAny(), It.IsAny())) + providerMock.Setup(x => x.ResolveStructureValue(flagName2, defaultStructureValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); var provider = providerMock.Object; diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs index f10e2d2c..6a389bc5 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs @@ -165,7 +165,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc // This will fail to case a String to TestStructure mockedFeatureProvider - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, "Mismatch")); mockedFeatureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -179,7 +179,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch.GetDescription()); mockedFeatureProvider - .Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + .Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); mockedLogger.Verify( x => x.Log( @@ -202,7 +202,7 @@ public async Task Should_Resolve_BooleanValue() var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock - .Setup(x => x.ResolveBooleanValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveBooleanValue(flagName, defaultValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -214,7 +214,7 @@ public async Task Should_Resolve_BooleanValue() (await client.GetBooleanValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveBooleanValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + featureProviderMock.Verify(x => x.ResolveBooleanValue(flagName, defaultValue, It.IsAny()), Times.Once); } [Fact] @@ -228,7 +228,7 @@ public async Task Should_Resolve_StringValue() var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock - .Setup(x => x.ResolveStringValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveStringValue(flagName, defaultValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -240,7 +240,7 @@ public async Task Should_Resolve_StringValue() (await client.GetStringValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveStringValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + featureProviderMock.Verify(x => x.ResolveStringValue(flagName, defaultValue, It.IsAny()), Times.Once); } [Fact] @@ -254,7 +254,7 @@ public async Task Should_Resolve_IntegerValue() var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock - .Setup(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -266,7 +266,7 @@ public async Task Should_Resolve_IntegerValue() (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + featureProviderMock.Verify(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny()), Times.Once); } [Fact] @@ -280,7 +280,7 @@ public async Task Should_Resolve_DoubleValue() var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock - .Setup(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -292,7 +292,7 @@ public async Task Should_Resolve_DoubleValue() (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + featureProviderMock.Verify(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny()), Times.Once); } [Fact] @@ -306,7 +306,7 @@ public async Task Should_Resolve_StructureValue() var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -318,7 +318,7 @@ public async Task Should_Resolve_StructureValue() (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); } [Fact] @@ -332,7 +332,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null)) + .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) .Throws(new FeatureProviderException(ErrorType.ParseError)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); @@ -345,7 +345,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() response.ErrorType.Should().Be(ErrorType.ParseError.GetDescription()); response.Reason.Should().Be(Reason.Error); - featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny(), null), Times.Once); + featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); } [Fact] diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs index 89c0d415..4ac0e828 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs @@ -204,38 +204,38 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() var propHook = "4.3.4hook"; // setup a cascade of overwriting properties - OpenFeature.Instance.SetContext(new EvaluationContext + OpenFeature.Instance.SetContext(new EvaluationContext { [propGlobal] = true, [propGlobalToOverwrite] = false }); - var clientContext = new EvaluationContext + var clientContext = new EvaluationContext { [propClient] = true, [propGlobalToOverwrite] = true, [propClientToOverwrite] = false }; - var invocationContext = new EvaluationContext + var invocationContext = new EvaluationContext { [propInvocation] = true, [propClientToOverwrite] = true, [propInvocationToOverwrite] = false, }; - var hookContext = new EvaluationContext + var hookContext = new EvaluationContext { [propHook] = true, [propInvocationToOverwrite] = true, }; - var provider = new Mock(MockBehavior.Strict); - + var provider = new Mock(MockBehavior.Strict); + provider.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); provider.Setup(x => x.GetProviderHooks()) .Returns(Array.Empty()); - provider.Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null)) + provider.Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails("test", true)); OpenFeature.Instance.SetProvider(provider.Object); @@ -249,7 +249,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(new[] { hook.Object }, new Dictionary())); // after proper merging, all properties should equal true - provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => + provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => y.Get(propGlobal) && y.Get(propClient) && y.Get(propGlobalToOverwrite) @@ -257,7 +257,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() && y.Get(propClientToOverwrite) && y.Get(propHook) && y.Get(propInvocationToOverwrite) - ), It.IsAny()), Times.Once); + )), Times.Once); } [Fact] @@ -315,7 +315,7 @@ public async Task Hook_Should_Execute_In_Correct_Order() .ReturnsAsync(new EvaluationContext()); featureProvider.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null)) + .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails("test", false)); hook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), @@ -333,7 +333,7 @@ public async Task Hook_Should_Execute_In_Correct_Order() hook.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); hook.Verify(x => x.Finally(It.IsAny>(), It.IsAny>()), Times.Once); - featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Once); + featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -384,8 +384,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() .ReturnsAsync(new EvaluationContext()); featureProvider.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), - null)) + .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails("test", false)); hook2.InSequence(sequence).Setup(x => x.After(It.IsAny>(), @@ -413,7 +412,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() hook1.Verify(x => x.Before(It.IsAny>(), null), Times.Once); hook2.Verify(x => x.Before(It.IsAny>(), null), Times.Once); - featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Once); + featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); hook2.Verify(x => x.After(It.IsAny>(), It.IsAny>(), null), Times.Once); hook1.Verify(x => x.After(It.IsAny>(), It.IsAny>(), null), Times.Once); hook2.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); @@ -444,8 +443,7 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() .ReturnsAsync(new EvaluationContext()); featureProvider1.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), - null)) + .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new Exception()); hook2.InSequence(sequence).Setup(x => @@ -528,7 +526,7 @@ public async Task Hook_Hints_May_Be_Optional() .ReturnsAsync(evaluationContext); featureProvider.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), flagOptions)) + .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails("test", false)); hook.InSequence(sequence) @@ -547,7 +545,7 @@ public async Task Hook_Hints_May_Be_Optional() hook.Verify(x => x.Before(It.IsAny>(), defaultEmptyHookHints), Times.Once); hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints), Times.Once); hook.Verify(x => x.Finally(It.IsAny>(), defaultEmptyHookHints), Times.Once); - featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny(), flagOptions), Times.Once); + featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] diff --git a/test/OpenFeature.SDK.Tests/TestImplementations.cs b/test/OpenFeature.SDK.Tests/TestImplementations.cs index 0b2d29cd..851886db 100644 --- a/test/OpenFeature.SDK.Tests/TestImplementations.cs +++ b/test/OpenFeature.SDK.Tests/TestImplementations.cs @@ -53,36 +53,31 @@ public override Metadata GetMetadata() } public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveStructureValue(string flagKey, T defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) + EvaluationContext context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } From 01c0f3432c362258b8d54fe37e13f5c5ecd20be0 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 9 Sep 2022 02:21:45 +1200 Subject: [PATCH 023/316] Rework EvaluationContext to use Structure type (#53) * Rework EvaluationContext to use Structure type - Adds `Structure` type which represents JSON data - Adds `Value` wrapper class that houses the supported types (Structure, List, bool, int, double, Value) - `EvaluationContext` exposes methods to interact with its underlying keyvalue pairs of string, `Value` - Update interfaces to use `Structure` instead of generics Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> * Fix code coverage Accidentally removed coverlet.msbuild which is needed to codecov Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> * add tests and doc Signed-off-by: Todd Baert * Store only doubles, round to ints Signed-off-by: Todd Baert * Add xml comments Signed-off-by: Todd Baert Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Signed-off-by: Todd Baert Co-authored-by: Todd Baert --- src/OpenFeature.SDK/FeatureProvider.cs | 5 +- src/OpenFeature.SDK/IFeatureClient.cs | 4 +- .../Model/EvaluationContext.cs | 183 +++++++++++----- src/OpenFeature.SDK/Model/Structure.cs | 206 ++++++++++++++++++ src/OpenFeature.SDK/Model/Value.cs | 159 ++++++++++++++ src/OpenFeature.SDK/NoOpProvider.cs | 2 +- src/OpenFeature.SDK/OpenFeatureClient.cs | 8 +- .../FeatureProviderTests.cs | 10 +- .../OpenFeature.SDK.Tests.csproj | 4 + .../OpenFeatureClientTests.cs | 22 +- .../OpenFeatureEvaluationContextTests.cs | 92 ++++---- .../OpenFeatureHookTests.cs | 71 +++--- test/OpenFeature.SDK.Tests/StructureTests.cs | 133 +++++++++++ .../TestImplementations.cs | 10 +- test/OpenFeature.SDK.Tests/ValueTests.cs | 85 ++++++++ 15 files changed, 826 insertions(+), 168 deletions(-) create mode 100644 src/OpenFeature.SDK/Model/Structure.cs create mode 100644 src/OpenFeature.SDK/Model/Value.cs create mode 100644 test/OpenFeature.SDK.Tests/StructureTests.cs create mode 100644 test/OpenFeature.SDK.Tests/ValueTests.cs diff --git a/src/OpenFeature.SDK/FeatureProvider.cs b/src/OpenFeature.SDK/FeatureProvider.cs index 21c3aaa8..26f51793 100644 --- a/src/OpenFeature.SDK/FeatureProvider.cs +++ b/src/OpenFeature.SDK/FeatureProvider.cs @@ -72,14 +72,13 @@ public abstract Task> ResolveDoubleValue(string flagKe EvaluationContext context = null); /// - /// Resolves a structured feature flag + /// Resolves a structure feature flag /// /// Feature flag key /// Default value /// - /// Type of object /// - public abstract Task> ResolveStructureValue(string flagKey, T defaultValue, + public abstract Task> ResolveStructureValue(string flagKey, Structure defaultValue, EvaluationContext context = null); } } diff --git a/src/OpenFeature.SDK/IFeatureClient.cs b/src/OpenFeature.SDK/IFeatureClient.cs index 8150f3e7..722c5927 100644 --- a/src/OpenFeature.SDK/IFeatureClient.cs +++ b/src/OpenFeature.SDK/IFeatureClient.cs @@ -21,7 +21,7 @@ internal interface IFeatureClient Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); - Task GetObjectValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); - Task> GetObjectDetails(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetObjectValue(string flagKey, Structure defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetObjectDetails(string flagKey, Structure defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); } } diff --git a/src/OpenFeature.SDK/Model/EvaluationContext.cs b/src/OpenFeature.SDK/Model/EvaluationContext.cs index 038217dc..b8ce9afa 100644 --- a/src/OpenFeature.SDK/Model/EvaluationContext.cs +++ b/src/OpenFeature.SDK/Model/EvaluationContext.cs @@ -1,7 +1,5 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace OpenFeature.SDK.Model { @@ -10,96 +8,175 @@ namespace OpenFeature.SDK.Model /// to the feature flag evaluation context. /// /// Evaluation context - public class EvaluationContext : IEnumerable> + public class EvaluationContext { - private readonly Dictionary _internalContext = new Dictionary(); + private readonly Structure _structure = new Structure(); /// - /// Add a new key value pair to the evaluation context + /// Gets the Value at the specified key /// - /// Key - /// Value - /// Type of value - public void Add(string key, T value) + /// + /// + public Value GetValue(string key) => this._structure.GetValue(key); + + /// + /// Bool indicating if the specified key exists in the evaluation context + /// + /// + /// + public bool ContainsKey(string key) => this._structure.ContainsKey(key); + + /// + /// Removes the Value at the specified key + /// + /// + public void Remove(string key) => this._structure.Remove(key); + + /// + /// Gets the value associated with the specified key + /// + /// + /// + /// + public bool TryGetValue(string key, out Value value) => this._structure.TryGetValue(key, out value); + + /// + /// Gets all values as a Dictionary + /// + /// + public IDictionary AsDictionary() + { + return new Dictionary(this._structure.AsDictionary()); + } + + /// + /// Add a new bool Value to the evaluation context + /// + /// + /// + /// + public EvaluationContext Add(string key, bool value) + { + this._structure.Add(key, value); + return this; + } + + /// + /// Add a new string Value to the evaluation context + /// + /// + /// + /// + public EvaluationContext Add(string key, string value) + { + this._structure.Add(key, value); + return this; + } + + /// + /// Add a new int Value to the evaluation context + /// + /// + /// + /// + public EvaluationContext Add(string key, int value) + { + this._structure.Add(key, value); + return this; + } + + /// + /// Add a new double Value to the evaluation context + /// + /// + /// + /// + public EvaluationContext Add(string key, double value) { - this._internalContext.Add(key, value); + this._structure.Add(key, value); + return this; } /// - /// Remove an object by key from the evaluation context + /// Add a new DateTime Value to the evaluation context /// - /// Key - /// Key is null - public bool Remove(string key) + /// + /// + /// + public EvaluationContext Add(string key, DateTime value) { - return this._internalContext.Remove(key); + this._structure.Add(key, value); + return this; } /// - /// Get an object from evaluation context by key + /// Add a new Structure Value to the evaluation context /// - /// Key - /// Type of object - /// Object casted to provided type - /// A type mismatch occurs - public T Get(string key) + /// + /// + /// + public EvaluationContext Add(string key, Structure value) { - return (T)this._internalContext[key]; + this._structure.Add(key, value); + return this; } /// - /// Get value by key - /// - /// Note: this will not case the object to type. - /// This will need to be done by the caller + /// Add a new List Value to the evaluation context /// - /// Key - public object this[string key] + /// + /// + /// + public EvaluationContext Add(string key, List value) { - get => this._internalContext[key]; - set => this._internalContext[key] = value; + this._structure.Add(key, value); + return this; } /// - /// Merges provided evaluation context into this one - /// - /// Any duplicate keys will be overwritten + /// Add a new Value to the evaluation context + /// + /// + /// + /// + public EvaluationContext Add(string key, Value value) + { + this._structure.Add(key, value); + return this; + } + + /// + /// Return a count of all values + /// + public int Count => this._structure.Count; + + /// + /// Merges provided evaluation context into this one. + /// Any duplicate keys will be overwritten. /// /// public void Merge(EvaluationContext other) { - foreach (var key in other._internalContext.Keys) + foreach (var key in other._structure.Keys) { - if (this._internalContext.ContainsKey(key)) + if (this._structure.ContainsKey(key)) { - this._internalContext[key] = other._internalContext[key]; + this._structure[key] = other._structure[key]; } else { - this._internalContext.Add(key, other._internalContext[key]); + this._structure.Add(key, other._structure[key]); } } } /// - /// Returns the number of items in the evaluation context + /// Return an enumerator for all values /// - public int Count => this._internalContext.Count; - - /// - /// Returns an enumerator that iterates through the evaluation context - /// - /// Enumerator of the Evaluation context - [ExcludeFromCodeCoverage] - public IEnumerator> GetEnumerator() - { - return this._internalContext.GetEnumerator(); - } - - [ExcludeFromCodeCoverage] - IEnumerator IEnumerable.GetEnumerator() + /// + public IEnumerator> GetEnumerator() { - return this.GetEnumerator(); + return this._structure.GetEnumerator(); } } } diff --git a/src/OpenFeature.SDK/Model/Structure.cs b/src/OpenFeature.SDK/Model/Structure.cs new file mode 100644 index 00000000..508b4083 --- /dev/null +++ b/src/OpenFeature.SDK/Model/Structure.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature.SDK.Model +{ + /// + /// Structure represents a map of Values + /// + public class Structure : IEnumerable> + { + private readonly Dictionary _attributes; + + /// + /// Creates a new structure with an empty set of attributes + /// + public Structure() + { + this._attributes = new Dictionary(); + } + + /// + /// Creates a new structure with the supplied attributes + /// + /// + public Structure(IDictionary attributes) + { + this._attributes = new Dictionary(attributes); + } + + /// + /// Gets the Value at the specified key + /// + /// The key of the value to be retrieved + /// + public Value GetValue(string key) => this._attributes[key]; + + /// + /// Bool indicating if the specified key exists in the structure + /// + /// The key of the value to be retrieved + /// indicating the presence of the key. + public bool ContainsKey(string key) => this._attributes.ContainsKey(key); + + /// + /// Removes the Value at the specified key + /// + /// The key of the value to be retrieved + /// indicating the presence of the key. + public bool Remove(string key) => this._attributes.Remove(key); + + /// + /// Gets the value associated with the specified key by mutating the supplied value. + /// + /// The key of the value to be retrieved + /// value to be mutated + /// indicating the presence of the key. + public bool TryGetValue(string key, out Value value) => this._attributes.TryGetValue(key, out value); + + /// + /// Gets all values as a Dictionary + /// + /// New representation of this Structure + public IDictionary AsDictionary() + { + return new Dictionary(this._attributes); + } + + /// + /// Return the value at the supplied index + /// + /// The key of the value to be retrieved + public Value this[string key] + { + get => this._attributes[key]; + set => this._attributes[key] = value; + } + + /// + /// Return a collection containing all the keys in this structure + /// + public ICollection Keys => this._attributes.Keys; + + /// + /// Return a collection containing all the values in this structure + /// + public ICollection Values => this._attributes.Values; + + /// + /// Add a new bool Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, bool value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Add a new string Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, string value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Add a new int Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, int value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Add a new double Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, double value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Add a new DateTime Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, DateTime value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Add a new Structure Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, Structure value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Add a new List Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, IList value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Add a new Value to the structure + /// + /// The key of the value to be retrieved + /// The value to be added + /// This + public Structure Add(string key, Value value) + { + this._attributes.Add(key, new Value(value)); + return this; + } + + /// + /// Return a count of all values + /// + public int Count => this._attributes.Count; + + /// + /// Return an enumerator for all values + /// + /// + public IEnumerator> GetEnumerator() + { + return this._attributes.GetEnumerator(); + } + + [ExcludeFromCodeCoverage] + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/OpenFeature.SDK/Model/Value.cs b/src/OpenFeature.SDK/Model/Value.cs new file mode 100644 index 00000000..2746a858 --- /dev/null +++ b/src/OpenFeature.SDK/Model/Value.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace OpenFeature.SDK.Model +{ + /// + /// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format. + /// This intermediate representation provides a good medium of exchange. + /// + public class Value + { + private readonly object _innerValue; + + /// + /// Creates a Value with the inner value set to null + /// + public Value() => this._innerValue = null; + + /// + /// Creates a Value with the inner value to the inner value of the value param + /// + /// Value type + public Value(Value value) => this._innerValue = value._innerValue; + + /// + /// Creates a Value with the inner set to bool type + /// + /// Bool type + public Value(bool value) => this._innerValue = value; + + /// + /// Creates a Value by converting value to a double + /// + /// Int type + public Value(int value) => this._innerValue = Convert.ToDouble(value); + + /// + /// Creates a Value with the inner set to double type + /// + /// Double type + public Value(double value) => this._innerValue = value; + + /// + /// Creates a Value with the inner set to string type + /// + /// String type + public Value(string value) => this._innerValue = value; + + /// + /// Creates a Value with the inner set to structure type + /// + /// Structure type + public Value(Structure value) => this._innerValue = value; + + /// + /// Creates a Value with the inner set to list type + /// + /// List type + public Value(IList value) => this._innerValue = value; + + /// + /// Creates a Value with the inner set to DateTime type + /// + /// DateTime type + public Value(DateTime value) => this._innerValue = value; + + /// + /// Determines if inner value is null + /// + /// True if value is null + public bool IsNull() => this._innerValue is null; + + /// + /// Determines if inner value is bool + /// + /// True if value is bool + public bool IsBoolean() => this._innerValue is bool; + + /// + /// Determines if inner value is numeric + /// + /// True if value is double + public bool IsNumber() => this._innerValue is double; + + /// + /// Determines if inner value is string + /// + /// True if value is string + public bool IsString() => this._innerValue is string; + + /// + /// Determines if inner value is Structure + /// + /// True if value is Structure + public bool IsStructure() => this._innerValue is Structure; + + /// + /// Determines if inner value is list + /// + /// True if value is list + public bool IsList() => this._innerValue is IList; + + /// + /// Determines if inner value is DateTime + /// + /// True if value is DateTime + public bool IsDateTime() => this._innerValue is DateTime; + + /// + /// Returns the underlying int value + /// Value will be null if it isn't a integer + /// + /// Value as int + public int? AsInteger() => this.IsNumber() ? (int?)Convert.ToInt32((double?)this._innerValue) : null; + + /// + /// Returns the underlying bool value + /// Value will be null if it isn't a bool + /// + /// Value as bool + public bool? AsBoolean() => this.IsBoolean() ? (bool?)this._innerValue : null; + + /// + /// Returns the underlying double value + /// Value will be null if it isn't a double + /// + /// Value as int + public double? AsDouble() => this.IsNumber() ? (double?)this._innerValue : null; + + /// + /// Returns the underlying string value + /// Value will be null if it isn't a string + /// + /// Value as string + public string AsString() => this.IsString() ? (string)this._innerValue : null; + + /// + /// Returns the underlying Structure value + /// Value will be null if it isn't a Structure + /// + /// Value as Structure + public Structure AsStructure() => this.IsStructure() ? (Structure)this._innerValue : null; + + /// + /// Returns the underlying List value + /// Value will be null if it isn't a List + /// + /// Value as List + public IList AsList() => this.IsList() ? (IList)this._innerValue : null; + + /// + /// Returns the underlying DateTime value + /// Value will be null if it isn't a DateTime + /// + /// Value as DateTime + public DateTime? AsDateTime() => this.IsDateTime() ? (DateTime?)this._innerValue : null; + } +} diff --git a/src/OpenFeature.SDK/NoOpProvider.cs b/src/OpenFeature.SDK/NoOpProvider.cs index 015c7dc5..bd5dd3ea 100644 --- a/src/OpenFeature.SDK/NoOpProvider.cs +++ b/src/OpenFeature.SDK/NoOpProvider.cs @@ -33,7 +33,7 @@ public override Task> ResolveDoubleValue(string flagKe return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null) + public override Task> ResolveStructureValue(string flagKey, Structure defaultValue, EvaluationContext context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs index 13ae1f28..306a15be 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeature.SDK/OpenFeatureClient.cs @@ -176,26 +176,26 @@ await this.EvaluateFlag(this._featureProvider.ResolveDoubleValue, FlagValueType. defaultValue, context, config); /// - /// Resolves a object feature flag + /// Resolves a structure object feature flag /// /// Feature flag key /// Default value /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - public async Task GetObjectValue(string flagKey, T defaultValue, EvaluationContext context = null, + public async Task GetObjectValue(string flagKey, Structure defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => (await this.GetObjectDetails(flagKey, defaultValue, context, config)).Value; /// - /// Resolves a object feature flag + /// Resolves a structure object feature flag /// /// Feature flag key /// Default value /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - public async Task> GetObjectDetails(string flagKey, T defaultValue, + public async Task> GetObjectDetails(string flagKey, Structure defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this._featureProvider.ResolveStructureValue, FlagValueType.Object, flagKey, defaultValue, context, config); diff --git a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs index 541ee5c5..ef6e5e89 100644 --- a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs @@ -36,7 +36,7 @@ public async Task Provider_Must_Resolve_Flag_Values() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var provider = new NoOpFeatureProvider(); var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); @@ -51,7 +51,7 @@ public async Task Provider_Must_Resolve_Flag_Values() var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await provider.ResolveStringValue(flagName, defaultStringValue)).Should().BeEquivalentTo(stringResolutionDetails); - var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await provider.ResolveStructureValue(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureResolutionDetails); } @@ -66,7 +66,7 @@ public async Task Provider_Must_ErrorType() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var providerMock = new Mock(MockBehavior.Strict); providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny())) @@ -82,10 +82,10 @@ public async Task Provider_Must_ErrorType() .ReturnsAsync(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); providerMock.Setup(x => x.ResolveStructureValue(flagName, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); providerMock.Setup(x => x.ResolveStructureValue(flagName2, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); var provider = providerMock.Object; diff --git a/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj b/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj index 8da08d04..f9765bb6 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj +++ b/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj @@ -12,6 +12,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs index 6a389bc5..a252079f 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs @@ -68,7 +68,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); @@ -114,7 +114,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); @@ -140,7 +140,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() (await client.GetStringDetails(flagName, defaultStringValue, new EvaluationContext())).Should().BeEquivalentTo(stringFlagEvaluationDetails); (await client.GetStringDetails(flagName, defaultStringValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetObjectDetails(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); (await client.GetObjectDetails(flagName, defaultStructureValue, new EvaluationContext())).Should().BeEquivalentTo(structureFlagEvaluationDetails); (await client.GetObjectDetails(flagName, defaultStructureValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); @@ -159,14 +159,14 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var clientName = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + var defaultValue = fixture.Create(); var mockedFeatureProvider = new Mock(MockBehavior.Strict); var mockedLogger = new Mock>(MockBehavior.Default); // This will fail to case a String to TestStructure mockedFeatureProvider - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, "Mismatch")); + .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) + .Throws(); mockedFeatureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); mockedFeatureProvider.Setup(x => x.GetProviderHooks()) @@ -179,7 +179,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch.GetDescription()); mockedFeatureProvider - .Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); + .Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); mockedLogger.Verify( x => x.Log( @@ -302,12 +302,12 @@ public async Task Should_Resolve_StructureValue() var clientName = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + var defaultValue = fixture.Create(); var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) @@ -316,7 +316,7 @@ public async Task Should_Resolve_StructureValue() OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); - (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetObjectValue(flagName, defaultValue)).Should().Equal(defaultValue); featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); } @@ -328,7 +328,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var clientName = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + var defaultValue = fixture.Create(); var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs index 327a8003..808ffc8c 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs @@ -13,17 +13,16 @@ public class OpenFeatureEvaluationContextTests [Fact] public void Should_Merge_Two_Contexts() { - var context1 = new EvaluationContext(); - var context2 = new EvaluationContext(); - - context1.Add("key1", "value1"); - context2.Add("key2", "value2"); + var context1 = new EvaluationContext() + .Add("key1", "value1"); + var context2 = new EvaluationContext() + .Add("key2", "value2"); context1.Merge(context2); Assert.Equal(2, context1.Count); - Assert.Equal("value1", context1["key1"]); - Assert.Equal("value2", context1["key2"]); + Assert.Equal("value1", context1.GetValue("key1").AsString()); + Assert.Equal("value2", context1.GetValue("key2").AsString()); } [Fact] @@ -40,19 +39,11 @@ public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Cont context1.Merge(context2); Assert.Equal(2, context1.Count); - Assert.Equal("overriden_value", context1["key1"]); - Assert.Equal("value2", context1["key2"]); + Assert.Equal("overriden_value", context1.GetValue("key1").AsString()); + Assert.Equal("value2", context1.GetValue("key2").AsString()); context1.Remove("key1"); - Assert.Throws(() => context1["key1"]); - } - - [Fact] - public void Should_Be_Able_To_Set_Value_Via_Indexer() - { - var context = new EvaluationContext(); - context["key"] = "value"; - context["key"].Should().Be("value"); + Assert.Throws(() => context1.GetValue("key1")); } [Fact] @@ -62,28 +53,45 @@ public void EvaluationContext_Should_All_Types() { var fixture = new Fixture(); var now = fixture.Create(); - var structure = fixture.Create(); - var context = new EvaluationContext - { - { "key1", "value" }, - { "key2", 1 }, - { "key3", true }, - { "key4", now }, - { "key5", structure} - }; - - context.Get("key1").Should().Be("value"); - context.Get("key2").Should().Be(1); - context.Get("key3").Should().Be(true); - context.Get("key4").Should().Be(now); - context.Get("key5").Should().Be(structure); + var structure = fixture.Create(); + var context = new EvaluationContext() + .Add("key1", "value") + .Add("key2", 1) + .Add("key3", true) + .Add("key4", now) + .Add("key5", structure) + .Add("key6", 1.0); + + var value1 = context.GetValue("key1"); + value1.IsString().Should().BeTrue(); + value1.AsString().Should().Be("value"); + + var value2 = context.GetValue("key2"); + value2.IsNumber().Should().BeTrue(); + value2.AsInteger().Should().Be(1); + + var value3 = context.GetValue("key3"); + value3.IsBoolean().Should().Be(true); + value3.AsBoolean().Should().Be(true); + + var value4 = context.GetValue("key4"); + value4.IsDateTime().Should().BeTrue(); + value4.AsDateTime().Should().Be(now); + + var value5 = context.GetValue("key5"); + value5.IsStructure().Should().BeTrue(); + value5.AsStructure().Should().Equal(structure); + + var value6 = context.GetValue("key6"); + value6.IsNumber().Should().BeTrue(); + value6.AsDouble().Should().Be(1.0); } [Fact] [Specification("3.1.4", "The evaluation context fields MUST have an unique key.")] public void When_Duplicate_Key_Throw_Unique_Constraint() { - var context = new EvaluationContext { { "key", "value" } }; + var context = new EvaluationContext().Add("key", "value"); var exception = Assert.Throws(() => context.Add("key", "overriden_value")); exception.Message.Should().StartWith("An item with the same key has already been added."); @@ -93,20 +101,18 @@ public void When_Duplicate_Key_Throw_Unique_Constraint() [Specification("3.1.3", "The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")] public void Should_Be_Able_To_Get_All_Values() { - var context = new EvaluationContext - { - { "key1", "value1" }, - { "key2", "value2" }, - { "key3", "value3" }, - { "key4", "value4" }, - { "key5", "value5" } - }; + var context = new EvaluationContext() + .Add("key1", "value1") + .Add("key2", "value2") + .Add("key3", "value3") + .Add("key4", "value4") + .Add("key5", "value5"); // Iterate over key value pairs and check consistency var count = 0; foreach (var keyValue in context) { - context[keyValue.Key].Should().Be(keyValue.Value); + context.GetValue(keyValue.Key).AsString().Should().Be(keyValue.Value.AsString()); count++; } diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs index 4ac0e828..a673a006 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs @@ -127,19 +127,19 @@ public async Task Hooks_Should_Be_Called_In_Order() public void Hook_Context_Should_Not_Allow_Nulls() { Assert.Throws(() => - new HookContext(null, new TestStructure(), FlagValueType.Object, new ClientMetadata(null, null), + new HookContext(null, new Structure(), FlagValueType.Object, new ClientMetadata(null, null), new Metadata(null), new EvaluationContext())); Assert.Throws(() => - new HookContext("test", new TestStructure(), FlagValueType.Object, null, + new HookContext("test", new Structure(), FlagValueType.Object, null, new Metadata(null), new EvaluationContext())); Assert.Throws(() => - new HookContext("test", new TestStructure(), FlagValueType.Object, new ClientMetadata(null, null), + new HookContext("test", new Structure(), FlagValueType.Object, new ClientMetadata(null, null), null, new EvaluationContext())); Assert.Throws(() => - new HookContext("test", new TestStructure(), FlagValueType.Object, new ClientMetadata(null, null), + new HookContext("test", new Structure(), FlagValueType.Object, new ClientMetadata(null, null), new Metadata(null), null)); } @@ -150,8 +150,8 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() { var clientMetadata = new ClientMetadata("client", "1.0.0"); var providerMetadata = new Metadata("provider"); - var testStructure = new TestStructure(); - var context = new HookContext("test", testStructure, FlagValueType.Object, clientMetadata, + var testStructure = new Structure(); + var context = new HookContext("test", testStructure, FlagValueType.Object, clientMetadata, providerMetadata, new EvaluationContext()); context.ClientMetadata.Should().BeSameAs(clientMetadata); @@ -166,7 +166,7 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() [Specification("4.3.3", "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")] public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() { - var evaluationContext = new EvaluationContext { ["test"] = "test" }; + var evaluationContext = new EvaluationContext().Add("test", "test"); var hook1 = new Mock(MockBehavior.Strict); var hook2 = new Mock(MockBehavior.Strict); var hookContext = new HookContext("test", false, @@ -185,7 +185,7 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() new FlagEvaluationOptions(new[] { hook1.Object, hook2.Object }, new Dictionary())); hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.Get("test") == "test"), It.IsAny>()), Times.Once); + hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString() == "test"), It.IsAny>()), Times.Once); } [Fact] @@ -204,28 +204,23 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() var propHook = "4.3.4hook"; // setup a cascade of overwriting properties - OpenFeature.Instance.SetContext(new EvaluationContext - { - [propGlobal] = true, - [propGlobalToOverwrite] = false - }); - var clientContext = new EvaluationContext - { - [propClient] = true, - [propGlobalToOverwrite] = true, - [propClientToOverwrite] = false - }; - var invocationContext = new EvaluationContext - { - [propInvocation] = true, - [propClientToOverwrite] = true, - [propInvocationToOverwrite] = false, - }; - var hookContext = new EvaluationContext - { - [propHook] = true, - [propInvocationToOverwrite] = true, - }; + OpenFeature.Instance.SetContext(new EvaluationContext() + .Add(propGlobal, true) + .Add(propGlobalToOverwrite, false)); + + var clientContext = new EvaluationContext() + .Add(propClient, true) + .Add(propGlobalToOverwrite, true) + .Add(propClientToOverwrite, false); + + var invocationContext = new EvaluationContext() + .Add(propInvocation, true) + .Add(propClientToOverwrite, true) + .Add(propInvocationToOverwrite, false); + + var hookContext = new EvaluationContext() + .Add(propHook, true) + .Add(propInvocationToOverwrite, true); var provider = new Mock(MockBehavior.Strict); @@ -250,13 +245,13 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() // after proper merging, all properties should equal true provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => - y.Get(propGlobal) - && y.Get(propClient) - && y.Get(propGlobalToOverwrite) - && y.Get(propInvocation) - && y.Get(propClientToOverwrite) - && y.Get(propHook) - && y.Get(propInvocationToOverwrite) + (y.GetValue(propGlobal).AsBoolean() ?? false) + && (y.GetValue(propClient).AsBoolean() ?? false) + && (y.GetValue(propGlobalToOverwrite).AsBoolean() ?? false) + && (y.GetValue(propInvocation).AsBoolean() ?? false) + && (y.GetValue(propClientToOverwrite).AsBoolean() ?? false) + && (y.GetValue(propHook).AsBoolean() ?? false) + && (y.GetValue(propInvocationToOverwrite).AsBoolean() ?? false) )), Times.Once); } @@ -275,7 +270,7 @@ public async Task Hook_Should_Return_No_Errors() ["number"] = 1, ["boolean"] = true, ["datetime"] = DateTime.Now, - ["structure"] = new TestStructure() + ["structure"] = new Structure() }; var hookContext = new HookContext("test", false, FlagValueType.Boolean, new ClientMetadata(null, null), new Metadata(null), new EvaluationContext()); diff --git a/test/OpenFeature.SDK.Tests/StructureTests.cs b/test/OpenFeature.SDK.Tests/StructureTests.cs new file mode 100644 index 00000000..63949030 --- /dev/null +++ b/test/OpenFeature.SDK.Tests/StructureTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using OpenFeature.SDK.Model; +using Xunit; + +namespace OpenFeature.SDK.Tests +{ + public class StructureTests + { + [Fact] + public void No_Arg_Should_Contain_Empty_Attributes() + { + Structure structure = new Structure(); + Assert.Equal(0, structure.Count); + Assert.Equal(0, structure.AsDictionary().Keys.Count); + } + + [Fact] + public void Dictionary_Arg_Should_Contain_New_Dictionary() + { + string KEY = "key"; + IDictionary dictionary = new Dictionary(){ + { KEY, new Value(KEY) } + }; + Structure structure = new Structure(dictionary); + Assert.Equal(KEY, structure.AsDictionary()[KEY].AsString()); + Assert.NotSame(structure.AsDictionary(), dictionary); // should be a copy + } + + [Fact] + public void Add_And_Get_Add_And_Return_Values() + { + String BOOL_KEY = "bool"; + String STRING_KEY = "string"; + String INT_KEY = "int"; + String DOUBLE_KEY = "double"; + String DATE_KEY = "date"; + String STRUCT_KEY = "struct"; + String LIST_KEY = "list"; + String VALUE_KEY = "value"; + + bool BOOL_VAL = true; + String STRING_VAL = "val"; + int INT_VAL = 13; + double DOUBLE_VAL = .5; + DateTime DATE_VAL = DateTime.Now; + Structure STRUCT_VAL = new Structure(); + IList LIST_VAL = new List(); + Value VALUE_VAL = new Value(); + + Structure structure = new Structure(); + structure.Add(BOOL_KEY, BOOL_VAL); + structure.Add(STRING_KEY, STRING_VAL); + structure.Add(INT_KEY, INT_VAL); + structure.Add(DOUBLE_KEY, DOUBLE_VAL); + structure.Add(DATE_KEY, DATE_VAL); + structure.Add(STRUCT_KEY, STRUCT_VAL); + structure.Add(LIST_KEY, LIST_VAL); + structure.Add(VALUE_KEY, VALUE_VAL); + + Assert.Equal(BOOL_VAL, structure.GetValue(BOOL_KEY).AsBoolean()); + Assert.Equal(STRING_VAL, structure.GetValue(STRING_KEY).AsString()); + Assert.Equal(INT_VAL, structure.GetValue(INT_KEY).AsInteger()); + Assert.Equal(DOUBLE_VAL, structure.GetValue(DOUBLE_KEY).AsDouble()); + Assert.Equal(DATE_VAL, structure.GetValue(DATE_KEY).AsDateTime()); + Assert.Equal(STRUCT_VAL, structure.GetValue(STRUCT_KEY).AsStructure()); + Assert.Equal(LIST_VAL, structure.GetValue(LIST_KEY).AsList()); + Assert.True(structure.GetValue(VALUE_KEY).IsNull()); + } + + [Fact] + public void Remove_Should_Remove_Value() + { + String KEY = "key"; + bool VAL = true; + + Structure structure = new Structure(); + structure.Add(KEY, VAL); + Assert.Equal(1, structure.Count); + structure.Remove(KEY); + Assert.Equal(0, structure.Count); + } + + [Fact] + public void TryGetValue_Should_Return_Value() + { + String KEY = "key"; + String VAL = "val"; + + Structure structure = new Structure(); + structure.Add(KEY, VAL); + Value value; + Assert.True(structure.TryGetValue(KEY, out value)); + Assert.Equal(VAL, value.AsString()); + } + + [Fact] + public void Values_Should_Return_Values() + { + String KEY = "key"; + Value VAL = new Value("val"); + + Structure structure = new Structure(); + structure.Add(KEY, VAL); + Assert.Equal(1, structure.Values.Count); + } + + [Fact] + public void Keys_Should_Return_Keys() + { + String KEY = "key"; + Value VAL = new Value("val"); + + Structure structure = new Structure(); + structure.Add(KEY, VAL); + Assert.Equal(1, structure.Keys.Count); + Assert.True(structure.Keys.Contains(KEY)); + } + + [Fact] + public void GetEnumerator_Should_Return_Enumerator() + { + string KEY = "key"; + string VAL = "val"; + + Structure structure = new Structure(); + structure.Add(KEY, VAL); + IEnumerator> enumerator = structure.GetEnumerator(); + enumerator.MoveNext(); + Assert.Equal(VAL, enumerator.Current.Value.AsString()); + } + } +} diff --git a/test/OpenFeature.SDK.Tests/TestImplementations.cs b/test/OpenFeature.SDK.Tests/TestImplementations.cs index 851886db..59392685 100644 --- a/test/OpenFeature.SDK.Tests/TestImplementations.cs +++ b/test/OpenFeature.SDK.Tests/TestImplementations.cs @@ -5,12 +5,6 @@ namespace OpenFeature.SDK.Tests { - public class TestStructure - { - public string Name { get; set; } - public string Value { get; set; } - } - public class TestHookNoOverride : Hook { } public class TestHook : Hook @@ -76,10 +70,10 @@ public override Task> ResolveDoubleValue(string flagKe return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, T defaultValue, + public override Task> ResolveStructureValue(string flagKey, Structure defaultValue, EvaluationContext context = null) { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } } } diff --git a/test/OpenFeature.SDK.Tests/ValueTests.cs b/test/OpenFeature.SDK.Tests/ValueTests.cs new file mode 100644 index 00000000..fde2573a --- /dev/null +++ b/test/OpenFeature.SDK.Tests/ValueTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using OpenFeature.SDK.Model; +using Xunit; + +namespace OpenFeature.SDK.Tests +{ + public class ValueTests + { + [Fact] + public void No_Arg_Should_Contain_Null() + { + Value value = new Value(); + Assert.True(value.IsNull()); + } + + [Fact] + public void Bool_Arg_Should_Contain_Bool() + { + bool innerValue = true; + Value value = new Value(innerValue); + Assert.True(value.IsBoolean()); + Assert.Equal(innerValue, value.AsBoolean()); + } + + [Fact] + public void Numeric_Arg_Should_Return_Double_Or_Int() + { + double innerDoubleValue = .75; + Value doubleValue = new Value(innerDoubleValue); + Assert.True(doubleValue.IsNumber()); + Assert.Equal(1, doubleValue.AsInteger()); // should be rounded + Assert.Equal(.75, doubleValue.AsDouble()); + + int innerIntValue = 100; + Value intValue = new Value(innerIntValue); + Assert.True(intValue.IsNumber()); + Assert.Equal(innerIntValue, intValue.AsInteger()); + Assert.Equal(innerIntValue, intValue.AsDouble()); + } + + [Fact] + public void String_Arg_Should_Contain_String() + { + string innerValue = "hi!"; + Value value = new Value(innerValue); + Assert.True(value.IsString()); + Assert.Equal(innerValue, value.AsString()); + } + + + [Fact] + public void DateTime_Arg_Should_Contain_DateTime() + { + DateTime innerValue = new DateTime(); + Value value = new Value(innerValue); + Assert.True(value.IsDateTime()); + Assert.Equal(innerValue, value.AsDateTime()); + } + + [Fact] + public void Structure_Arg_Should_Contain_Structure() + { + string INNER_KEY = "key"; + string INNER_VALUE = "val"; + Structure innerValue = new Structure().Add(INNER_KEY, INNER_VALUE); + Value value = new Value(innerValue); + Assert.True(value.IsStructure()); + Assert.Equal(INNER_VALUE, value.AsStructure().GetValue(INNER_KEY).AsString()); + } + + [Fact] + public void LIst_Arg_Should_Contain_LIst() + { + string ITEM_VALUE = "val"; + IList innerValue = new List() + { + new Value(ITEM_VALUE) + }; + Value value = new Value(innerValue); + Assert.True(value.IsList()); + Assert.Equal(ITEM_VALUE, value.AsList()[0].AsString()); + } + } +} From acce643bb7a1da631b9dbee04b7ad1b15eb469b8 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 8 Sep 2022 11:45:33 -0400 Subject: [PATCH 024/316] Add setter for context Signed-off-by: Todd Baert --- src/OpenFeature.SDK/OpenFeatureClient.cs | 11 ++++++++--- test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs | 10 ++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs index 306a15be..d45ec675 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeature.SDK/OpenFeatureClient.cs @@ -20,14 +20,19 @@ public sealed class FeatureClient : IFeatureClient private readonly FeatureProvider _featureProvider; private readonly List _hooks = new List(); private readonly ILogger _logger; - private readonly EvaluationContext _evaluationContext; + private EvaluationContext _evaluationContext; /// - /// Gets the client + /// Gets the EvaluationContext of this client /// - /// + /// of this client public EvaluationContext GetContext() => this._evaluationContext; + /// + /// Sets the EvaluationContext of the client + /// + public void SetContext(EvaluationContext evaluationContext) => this._evaluationContext = evaluationContext; + /// /// Initializes a new instance of the class. /// diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs index a252079f..3d9b9fbd 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs @@ -354,5 +354,15 @@ public void Should_Throw_ArgumentNullException_When_Provider_Is_Null() TestProvider provider = null; Assert.Throws(() => new FeatureClient(provider, "test", "test")); } + + [Fact] + public void Should_Get_And_Set_Context() + { + var KEY = "key"; + var VAL = 1; + FeatureClient client = OpenFeature.Instance.GetClient(); + client.SetContext(new EvaluationContext().Add(KEY, VAL)); + Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger()); + } } } From f97d0228c7d9f8946494dfeb3df82ded680a41c9 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Sep 2022 13:55:35 -0400 Subject: [PATCH 025/316] feat!: structure->value, object value constructor Signed-off-by: Todd Baert --- src/OpenFeature.SDK/FeatureProvider.cs | 4 +- src/OpenFeature.SDK/IFeatureClient.cs | 4 +- src/OpenFeature.SDK/Model/Value.cs | 27 ++++++++++ src/OpenFeature.SDK/NoOpProvider.cs | 2 +- src/OpenFeature.SDK/OpenFeatureClient.cs | 4 +- .../FeatureProviderTests.cs | 10 ++-- .../OpenFeatureClientTests.cs | 16 +++--- .../TestImplementations.cs | 4 +- test/OpenFeature.SDK.Tests/ValueTests.cs | 52 ++++++++++++++++++- 9 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/OpenFeature.SDK/FeatureProvider.cs b/src/OpenFeature.SDK/FeatureProvider.cs index 26f51793..648f5db1 100644 --- a/src/OpenFeature.SDK/FeatureProvider.cs +++ b/src/OpenFeature.SDK/FeatureProvider.cs @@ -72,13 +72,13 @@ public abstract Task> ResolveDoubleValue(string flagKe EvaluationContext context = null); /// - /// Resolves a structure feature flag + /// Resolves a structured feature flag /// /// Feature flag key /// Default value /// /// - public abstract Task> ResolveStructureValue(string flagKey, Structure defaultValue, + public abstract Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null); } } diff --git a/src/OpenFeature.SDK/IFeatureClient.cs b/src/OpenFeature.SDK/IFeatureClient.cs index 722c5927..99af15c3 100644 --- a/src/OpenFeature.SDK/IFeatureClient.cs +++ b/src/OpenFeature.SDK/IFeatureClient.cs @@ -21,7 +21,7 @@ internal interface IFeatureClient Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); - Task GetObjectValue(string flagKey, Structure defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); - Task> GetObjectDetails(string flagKey, Structure defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); } } diff --git a/src/OpenFeature.SDK/Model/Value.cs b/src/OpenFeature.SDK/Model/Value.cs index 2746a858..b805df98 100644 --- a/src/OpenFeature.SDK/Model/Value.cs +++ b/src/OpenFeature.SDK/Model/Value.cs @@ -17,6 +17,27 @@ public class Value /// public Value() => this._innerValue = null; + /// + /// Creates a Value with the inner set to the object + /// + /// The object to set as the inner value + public Value(Object value) + { + // integer is a special case, convert those. + this._innerValue = value is int ? Convert.ToDouble(value) : value; + if (!(this.IsNull() + || this.IsBoolean() + || this.IsString() + || this.IsNumber() + || this.IsStructure() + || this.IsList() + || this.IsDateTime())) + { + throw new ArgumentException("Invalid value type: " + value.GetType()); + } + } + + /// /// Creates a Value with the inner value to the inner value of the value param /// @@ -107,6 +128,12 @@ public class Value /// True if value is DateTime public bool IsDateTime() => this._innerValue is DateTime; + /// + /// Returns the underlying inner value as an object. Returns null if the inner value is null. + /// + /// Value as object + public object AsObject() => this._innerValue; + /// /// Returns the underlying int value /// Value will be null if it isn't a integer diff --git a/src/OpenFeature.SDK/NoOpProvider.cs b/src/OpenFeature.SDK/NoOpProvider.cs index bd5dd3ea..a406a994 100644 --- a/src/OpenFeature.SDK/NoOpProvider.cs +++ b/src/OpenFeature.SDK/NoOpProvider.cs @@ -33,7 +33,7 @@ public override Task> ResolveDoubleValue(string flagKe return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Structure defaultValue, EvaluationContext context = null) + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs index d45ec675..04125fec 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeature.SDK/OpenFeatureClient.cs @@ -188,7 +188,7 @@ await this.EvaluateFlag(this._featureProvider.ResolveDoubleValue, FlagValueType. /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - public async Task GetObjectValue(string flagKey, Structure defaultValue, EvaluationContext context = null, + public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => (await this.GetObjectDetails(flagKey, defaultValue, context, config)).Value; @@ -200,7 +200,7 @@ public async Task GetObjectValue(string flagKey, Structure defaultVal /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - public async Task> GetObjectDetails(string flagKey, Structure defaultValue, + public async Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this._featureProvider.ResolveStructureValue, FlagValueType.Object, flagKey, defaultValue, context, config); diff --git a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs index ef6e5e89..8965eff0 100644 --- a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs @@ -36,7 +36,7 @@ public async Task Provider_Must_Resolve_Flag_Values() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var provider = new NoOpFeatureProvider(); var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); @@ -51,7 +51,7 @@ public async Task Provider_Must_Resolve_Flag_Values() var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await provider.ResolveStringValue(flagName, defaultStringValue)).Should().BeEquivalentTo(stringResolutionDetails); - var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await provider.ResolveStructureValue(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureResolutionDetails); } @@ -66,7 +66,7 @@ public async Task Provider_Must_ErrorType() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var providerMock = new Mock(MockBehavior.Strict); providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny())) @@ -82,10 +82,10 @@ public async Task Provider_Must_ErrorType() .ReturnsAsync(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); providerMock.Setup(x => x.ResolveStructureValue(flagName, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); providerMock.Setup(x => x.ResolveStructureValue(flagName2, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); var provider = providerMock.Object; diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs index 3d9b9fbd..ac32c42c 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs @@ -68,7 +68,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); @@ -114,7 +114,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var defaultStringValue = fixture.Create(); var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); @@ -140,7 +140,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() (await client.GetStringDetails(flagName, defaultStringValue, new EvaluationContext())).Should().BeEquivalentTo(stringFlagEvaluationDetails); (await client.GetStringDetails(flagName, defaultStringValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetObjectDetails(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); (await client.GetObjectDetails(flagName, defaultStructureValue, new EvaluationContext())).Should().BeEquivalentTo(structureFlagEvaluationDetails); (await client.GetObjectDetails(flagName, defaultStructureValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); @@ -159,7 +159,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var clientName = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + var defaultValue = fixture.Create(); var mockedFeatureProvider = new Mock(MockBehavior.Strict); var mockedLogger = new Mock>(MockBehavior.Default); @@ -302,12 +302,12 @@ public async Task Should_Resolve_StructureValue() var clientName = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + var defaultValue = fixture.Create(); var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) @@ -316,7 +316,7 @@ public async Task Should_Resolve_StructureValue() OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); - (await client.GetObjectValue(flagName, defaultValue)).Should().Equal(defaultValue); + (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); } @@ -328,7 +328,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var clientName = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + var defaultValue = fixture.Create(); var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock diff --git a/test/OpenFeature.SDK.Tests/TestImplementations.cs b/test/OpenFeature.SDK.Tests/TestImplementations.cs index 59392685..1d0539aa 100644 --- a/test/OpenFeature.SDK.Tests/TestImplementations.cs +++ b/test/OpenFeature.SDK.Tests/TestImplementations.cs @@ -70,10 +70,10 @@ public override Task> ResolveDoubleValue(string flagKe return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Structure defaultValue, + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } } } diff --git a/test/OpenFeature.SDK.Tests/ValueTests.cs b/test/OpenFeature.SDK.Tests/ValueTests.cs index fde2573a..1129b808 100644 --- a/test/OpenFeature.SDK.Tests/ValueTests.cs +++ b/test/OpenFeature.SDK.Tests/ValueTests.cs @@ -7,6 +7,8 @@ namespace OpenFeature.SDK.Tests { public class ValueTests { + class Foo { } + [Fact] public void No_Arg_Should_Contain_Null() { @@ -14,6 +16,55 @@ public void No_Arg_Should_Contain_Null() Assert.True(value.IsNull()); } + [Fact] + public void Object_Arg_Should_Contain_Object() + { + try + { + // int is a special case, see Int_Object_Arg_Should_Contain_Object() + IList list = new List(){ + true, "val", .5, new Structure(), new List(), DateTime.Now + }; + + int i = 0; + foreach (Object l in list) + { + Value value = new Value(l); + Assert.Equal(list[i], value.AsObject()); + i++; + } + } + catch (Exception) + { + Assert.True(false, "Expected no exception."); + } + } + + [Fact] + public void Int_Object_Arg_Should_Contain_Object() + { + try + { + int innerValue = 1; + Value value = new Value(innerValue); + Assert.True(value.IsNumber()); + Assert.Equal(innerValue, value.AsInteger()); + } + catch (Exception) + { + Assert.True(false, "Expected no exception."); + } + } + + [Fact] + public void Invalid_Object_Should_Throw() + { + Assert.Throws(() => + { + return new Value(new Foo()); + }); + } + [Fact] public void Bool_Arg_Should_Contain_Bool() { @@ -48,7 +99,6 @@ public void String_Arg_Should_Contain_String() Assert.Equal(innerValue, value.AsString()); } - [Fact] public void DateTime_Arg_Should_Contain_DateTime() { From 9f8bdf580cb3d7a9d804e84a1573cfda20145e77 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 12 Sep 2022 08:48:18 -0400 Subject: [PATCH 026/316] Convert arg-less methods to props Signed-off-by: Todd Baert --- src/OpenFeature.SDK/Model/Value.cs | 52 ++++++++--------- .../OpenFeatureClientTests.cs | 2 +- .../OpenFeatureEvaluationContextTests.cs | 34 +++++------ .../OpenFeatureHookTests.cs | 16 +++--- test/OpenFeature.SDK.Tests/StructureTests.cs | 22 ++++---- test/OpenFeature.SDK.Tests/ValueTests.cs | 56 +++++++++---------- 6 files changed, 91 insertions(+), 91 deletions(-) diff --git a/src/OpenFeature.SDK/Model/Value.cs b/src/OpenFeature.SDK/Model/Value.cs index b805df98..941eecba 100644 --- a/src/OpenFeature.SDK/Model/Value.cs +++ b/src/OpenFeature.SDK/Model/Value.cs @@ -21,23 +21,23 @@ public class Value /// Creates a Value with the inner set to the object /// /// The object to set as the inner value - public Value(Object value) + public Value(Object value) { // integer is a special case, convert those. this._innerValue = value is int ? Convert.ToDouble(value) : value; - if (!(this.IsNull() - || this.IsBoolean() - || this.IsString() - || this.IsNumber() - || this.IsStructure() - || this.IsList() - || this.IsDateTime())) + if (!(this.IsNull + || this.IsBoolean + || this.IsString + || this.IsNumber + || this.IsStructure + || this.IsList + || this.IsDateTime)) { throw new ArgumentException("Invalid value type: " + value.GetType()); } - } - - + } + + /// /// Creates a Value with the inner value to the inner value of the value param /// @@ -90,97 +90,97 @@ public Value(Object value) /// Determines if inner value is null /// /// True if value is null - public bool IsNull() => this._innerValue is null; + public bool IsNull => this._innerValue is null; /// /// Determines if inner value is bool /// /// True if value is bool - public bool IsBoolean() => this._innerValue is bool; + public bool IsBoolean => this._innerValue is bool; /// /// Determines if inner value is numeric /// /// True if value is double - public bool IsNumber() => this._innerValue is double; + public bool IsNumber => this._innerValue is double; /// /// Determines if inner value is string /// /// True if value is string - public bool IsString() => this._innerValue is string; + public bool IsString => this._innerValue is string; /// /// Determines if inner value is Structure /// /// True if value is Structure - public bool IsStructure() => this._innerValue is Structure; + public bool IsStructure => this._innerValue is Structure; /// /// Determines if inner value is list /// /// True if value is list - public bool IsList() => this._innerValue is IList; + public bool IsList => this._innerValue is IList; /// /// Determines if inner value is DateTime /// /// True if value is DateTime - public bool IsDateTime() => this._innerValue is DateTime; + public bool IsDateTime => this._innerValue is DateTime; /// /// Returns the underlying inner value as an object. Returns null if the inner value is null. /// /// Value as object - public object AsObject() => this._innerValue; + public object AsObject => this._innerValue; /// /// Returns the underlying int value /// Value will be null if it isn't a integer /// /// Value as int - public int? AsInteger() => this.IsNumber() ? (int?)Convert.ToInt32((double?)this._innerValue) : null; + public int? AsInteger => this.IsNumber ? (int?)Convert.ToInt32((double?)this._innerValue) : null; /// /// Returns the underlying bool value /// Value will be null if it isn't a bool /// /// Value as bool - public bool? AsBoolean() => this.IsBoolean() ? (bool?)this._innerValue : null; + public bool? AsBoolean => this.IsBoolean ? (bool?)this._innerValue : null; /// /// Returns the underlying double value /// Value will be null if it isn't a double /// /// Value as int - public double? AsDouble() => this.IsNumber() ? (double?)this._innerValue : null; + public double? AsDouble => this.IsNumber ? (double?)this._innerValue : null; /// /// Returns the underlying string value /// Value will be null if it isn't a string /// /// Value as string - public string AsString() => this.IsString() ? (string)this._innerValue : null; + public string AsString => this.IsString ? (string)this._innerValue : null; /// /// Returns the underlying Structure value /// Value will be null if it isn't a Structure /// /// Value as Structure - public Structure AsStructure() => this.IsStructure() ? (Structure)this._innerValue : null; + public Structure AsStructure => this.IsStructure ? (Structure)this._innerValue : null; /// /// Returns the underlying List value /// Value will be null if it isn't a List /// /// Value as List - public IList AsList() => this.IsList() ? (IList)this._innerValue : null; + public IList AsList => this.IsList ? (IList)this._innerValue : null; /// /// Returns the underlying DateTime value /// Value will be null if it isn't a DateTime /// /// Value as DateTime - public DateTime? AsDateTime() => this.IsDateTime() ? (DateTime?)this._innerValue : null; + public DateTime? AsDateTime => this.IsDateTime ? (DateTime?)this._innerValue : null; } } diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs index ac32c42c..5902908e 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs @@ -362,7 +362,7 @@ public void Should_Get_And_Set_Context() var VAL = 1; FeatureClient client = OpenFeature.Instance.GetClient(); client.SetContext(new EvaluationContext().Add(KEY, VAL)); - Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger()); + Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } } } diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs index 808ffc8c..ff913c13 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs @@ -21,8 +21,8 @@ public void Should_Merge_Two_Contexts() context1.Merge(context2); Assert.Equal(2, context1.Count); - Assert.Equal("value1", context1.GetValue("key1").AsString()); - Assert.Equal("value2", context1.GetValue("key2").AsString()); + Assert.Equal("value1", context1.GetValue("key1").AsString); + Assert.Equal("value2", context1.GetValue("key2").AsString); } [Fact] @@ -39,8 +39,8 @@ public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Cont context1.Merge(context2); Assert.Equal(2, context1.Count); - Assert.Equal("overriden_value", context1.GetValue("key1").AsString()); - Assert.Equal("value2", context1.GetValue("key2").AsString()); + Assert.Equal("overriden_value", context1.GetValue("key1").AsString); + Assert.Equal("value2", context1.GetValue("key2").AsString); context1.Remove("key1"); Assert.Throws(() => context1.GetValue("key1")); @@ -63,28 +63,28 @@ public void EvaluationContext_Should_All_Types() .Add("key6", 1.0); var value1 = context.GetValue("key1"); - value1.IsString().Should().BeTrue(); - value1.AsString().Should().Be("value"); + value1.IsString.Should().BeTrue(); + value1.AsString.Should().Be("value"); var value2 = context.GetValue("key2"); - value2.IsNumber().Should().BeTrue(); - value2.AsInteger().Should().Be(1); + value2.IsNumber.Should().BeTrue(); + value2.AsInteger.Should().Be(1); var value3 = context.GetValue("key3"); - value3.IsBoolean().Should().Be(true); - value3.AsBoolean().Should().Be(true); + value3.IsBoolean.Should().Be(true); + value3.AsBoolean.Should().Be(true); var value4 = context.GetValue("key4"); - value4.IsDateTime().Should().BeTrue(); - value4.AsDateTime().Should().Be(now); + value4.IsDateTime.Should().BeTrue(); + value4.AsDateTime.Should().Be(now); var value5 = context.GetValue("key5"); - value5.IsStructure().Should().BeTrue(); - value5.AsStructure().Should().Equal(structure); + value5.IsStructure.Should().BeTrue(); + value5.AsStructure.Should().Equal(structure); var value6 = context.GetValue("key6"); - value6.IsNumber().Should().BeTrue(); - value6.AsDouble().Should().Be(1.0); + value6.IsNumber.Should().BeTrue(); + value6.AsDouble.Should().Be(1.0); } [Fact] @@ -112,7 +112,7 @@ public void Should_Be_Able_To_Get_All_Values() var count = 0; foreach (var keyValue in context) { - context.GetValue(keyValue.Key).AsString().Should().Be(keyValue.Value.AsString()); + context.GetValue(keyValue.Key).AsString.Should().Be(keyValue.Value.AsString); count++; } diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs index a673a006..7b470be5 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs @@ -185,7 +185,7 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() new FlagEvaluationOptions(new[] { hook1.Object, hook2.Object }, new Dictionary())); hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString() == "test"), It.IsAny>()), Times.Once); + hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), It.IsAny>()), Times.Once); } [Fact] @@ -245,13 +245,13 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() // after proper merging, all properties should equal true provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => - (y.GetValue(propGlobal).AsBoolean() ?? false) - && (y.GetValue(propClient).AsBoolean() ?? false) - && (y.GetValue(propGlobalToOverwrite).AsBoolean() ?? false) - && (y.GetValue(propInvocation).AsBoolean() ?? false) - && (y.GetValue(propClientToOverwrite).AsBoolean() ?? false) - && (y.GetValue(propHook).AsBoolean() ?? false) - && (y.GetValue(propInvocationToOverwrite).AsBoolean() ?? false) + (y.GetValue(propGlobal).AsBoolean ?? false) + && (y.GetValue(propClient).AsBoolean ?? false) + && (y.GetValue(propGlobalToOverwrite).AsBoolean ?? false) + && (y.GetValue(propInvocation).AsBoolean ?? false) + && (y.GetValue(propClientToOverwrite).AsBoolean ?? false) + && (y.GetValue(propHook).AsBoolean ?? false) + && (y.GetValue(propInvocationToOverwrite).AsBoolean ?? false) )), Times.Once); } diff --git a/test/OpenFeature.SDK.Tests/StructureTests.cs b/test/OpenFeature.SDK.Tests/StructureTests.cs index 63949030..e16757e8 100644 --- a/test/OpenFeature.SDK.Tests/StructureTests.cs +++ b/test/OpenFeature.SDK.Tests/StructureTests.cs @@ -23,7 +23,7 @@ public void Dictionary_Arg_Should_Contain_New_Dictionary() { KEY, new Value(KEY) } }; Structure structure = new Structure(dictionary); - Assert.Equal(KEY, structure.AsDictionary()[KEY].AsString()); + Assert.Equal(KEY, structure.AsDictionary()[KEY].AsString); Assert.NotSame(structure.AsDictionary(), dictionary); // should be a copy } @@ -58,14 +58,14 @@ public void Add_And_Get_Add_And_Return_Values() structure.Add(LIST_KEY, LIST_VAL); structure.Add(VALUE_KEY, VALUE_VAL); - Assert.Equal(BOOL_VAL, structure.GetValue(BOOL_KEY).AsBoolean()); - Assert.Equal(STRING_VAL, structure.GetValue(STRING_KEY).AsString()); - Assert.Equal(INT_VAL, structure.GetValue(INT_KEY).AsInteger()); - Assert.Equal(DOUBLE_VAL, structure.GetValue(DOUBLE_KEY).AsDouble()); - Assert.Equal(DATE_VAL, structure.GetValue(DATE_KEY).AsDateTime()); - Assert.Equal(STRUCT_VAL, structure.GetValue(STRUCT_KEY).AsStructure()); - Assert.Equal(LIST_VAL, structure.GetValue(LIST_KEY).AsList()); - Assert.True(structure.GetValue(VALUE_KEY).IsNull()); + Assert.Equal(BOOL_VAL, structure.GetValue(BOOL_KEY).AsBoolean); + Assert.Equal(STRING_VAL, structure.GetValue(STRING_KEY).AsString); + Assert.Equal(INT_VAL, structure.GetValue(INT_KEY).AsInteger); + Assert.Equal(DOUBLE_VAL, structure.GetValue(DOUBLE_KEY).AsDouble); + Assert.Equal(DATE_VAL, structure.GetValue(DATE_KEY).AsDateTime); + Assert.Equal(STRUCT_VAL, structure.GetValue(STRUCT_KEY).AsStructure); + Assert.Equal(LIST_VAL, structure.GetValue(LIST_KEY).AsList); + Assert.True(structure.GetValue(VALUE_KEY).IsNull); } [Fact] @@ -91,7 +91,7 @@ public void TryGetValue_Should_Return_Value() structure.Add(KEY, VAL); Value value; Assert.True(structure.TryGetValue(KEY, out value)); - Assert.Equal(VAL, value.AsString()); + Assert.Equal(VAL, value.AsString); } [Fact] @@ -127,7 +127,7 @@ public void GetEnumerator_Should_Return_Enumerator() structure.Add(KEY, VAL); IEnumerator> enumerator = structure.GetEnumerator(); enumerator.MoveNext(); - Assert.Equal(VAL, enumerator.Current.Value.AsString()); + Assert.Equal(VAL, enumerator.Current.Value.AsString); } } } diff --git a/test/OpenFeature.SDK.Tests/ValueTests.cs b/test/OpenFeature.SDK.Tests/ValueTests.cs index 1129b808..47d4876a 100644 --- a/test/OpenFeature.SDK.Tests/ValueTests.cs +++ b/test/OpenFeature.SDK.Tests/ValueTests.cs @@ -13,13 +13,13 @@ class Foo { } public void No_Arg_Should_Contain_Null() { Value value = new Value(); - Assert.True(value.IsNull()); + Assert.True(value.IsNull); } [Fact] public void Object_Arg_Should_Contain_Object() { - try + try { // int is a special case, see Int_Object_Arg_Should_Contain_Object() IList list = new List(){ @@ -27,14 +27,14 @@ public void Object_Arg_Should_Contain_Object() }; int i = 0; - foreach (Object l in list) + foreach (Object l in list) { Value value = new Value(l); - Assert.Equal(list[i], value.AsObject()); + Assert.Equal(list[i], value.AsObject); i++; } - } - catch (Exception) + } + catch (Exception) { Assert.True(false, "Expected no exception."); } @@ -43,14 +43,14 @@ public void Object_Arg_Should_Contain_Object() [Fact] public void Int_Object_Arg_Should_Contain_Object() { - try + try { int innerValue = 1; Value value = new Value(innerValue); - Assert.True(value.IsNumber()); - Assert.Equal(innerValue, value.AsInteger()); - } - catch (Exception) + Assert.True(value.IsNumber); + Assert.Equal(innerValue, value.AsInteger); + } + catch (Exception) { Assert.True(false, "Expected no exception."); } @@ -59,7 +59,7 @@ public void Int_Object_Arg_Should_Contain_Object() [Fact] public void Invalid_Object_Should_Throw() { - Assert.Throws(() => + Assert.Throws(() => { return new Value(new Foo()); }); @@ -70,8 +70,8 @@ public void Bool_Arg_Should_Contain_Bool() { bool innerValue = true; Value value = new Value(innerValue); - Assert.True(value.IsBoolean()); - Assert.Equal(innerValue, value.AsBoolean()); + Assert.True(value.IsBoolean); + Assert.Equal(innerValue, value.AsBoolean); } [Fact] @@ -79,15 +79,15 @@ public void Numeric_Arg_Should_Return_Double_Or_Int() { double innerDoubleValue = .75; Value doubleValue = new Value(innerDoubleValue); - Assert.True(doubleValue.IsNumber()); - Assert.Equal(1, doubleValue.AsInteger()); // should be rounded - Assert.Equal(.75, doubleValue.AsDouble()); + Assert.True(doubleValue.IsNumber); + Assert.Equal(1, doubleValue.AsInteger); // should be rounded + Assert.Equal(.75, doubleValue.AsDouble); int innerIntValue = 100; Value intValue = new Value(innerIntValue); - Assert.True(intValue.IsNumber()); - Assert.Equal(innerIntValue, intValue.AsInteger()); - Assert.Equal(innerIntValue, intValue.AsDouble()); + Assert.True(intValue.IsNumber); + Assert.Equal(innerIntValue, intValue.AsInteger); + Assert.Equal(innerIntValue, intValue.AsDouble); } [Fact] @@ -95,8 +95,8 @@ public void String_Arg_Should_Contain_String() { string innerValue = "hi!"; Value value = new Value(innerValue); - Assert.True(value.IsString()); - Assert.Equal(innerValue, value.AsString()); + Assert.True(value.IsString); + Assert.Equal(innerValue, value.AsString); } [Fact] @@ -104,8 +104,8 @@ public void DateTime_Arg_Should_Contain_DateTime() { DateTime innerValue = new DateTime(); Value value = new Value(innerValue); - Assert.True(value.IsDateTime()); - Assert.Equal(innerValue, value.AsDateTime()); + Assert.True(value.IsDateTime); + Assert.Equal(innerValue, value.AsDateTime); } [Fact] @@ -115,8 +115,8 @@ public void Structure_Arg_Should_Contain_Structure() string INNER_VALUE = "val"; Structure innerValue = new Structure().Add(INNER_KEY, INNER_VALUE); Value value = new Value(innerValue); - Assert.True(value.IsStructure()); - Assert.Equal(INNER_VALUE, value.AsStructure().GetValue(INNER_KEY).AsString()); + Assert.True(value.IsStructure); + Assert.Equal(INNER_VALUE, value.AsStructure.GetValue(INNER_KEY).AsString); } [Fact] @@ -128,8 +128,8 @@ public void LIst_Arg_Should_Contain_LIst() new Value(ITEM_VALUE) }; Value value = new Value(innerValue); - Assert.True(value.IsList()); - Assert.Equal(ITEM_VALUE, value.AsList()[0].AsString()); + Assert.True(value.IsList); + Assert.Equal(ITEM_VALUE, value.AsList[0].AsString); } } } From 5771f01af46d1a89cf84bb15829a0cb3e3fb6db3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 13 Sep 2022 12:00:24 -0700 Subject: [PATCH 027/316] Document EvaluationContext members. Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- .../Model/EvaluationContext.cs | 131 +++++++++++++----- 1 file changed, 97 insertions(+), 34 deletions(-) diff --git a/src/OpenFeature.SDK/Model/EvaluationContext.cs b/src/OpenFeature.SDK/Model/EvaluationContext.cs index b8ce9afa..276681b8 100644 --- a/src/OpenFeature.SDK/Model/EvaluationContext.cs +++ b/src/OpenFeature.SDK/Model/EvaluationContext.cs @@ -15,35 +15,50 @@ public class 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); /// /// Removes the Value at the specified key /// - /// + /// The key of the value to be removed + /// + /// Thrown when the key is . + /// public void Remove(string key) => this._structure.Remove(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 IDictionary AsDictionary() { return new Dictionary(this._structure.AsDictionary()); @@ -52,9 +67,15 @@ public IDictionary AsDictionary() /// /// Add a new bool Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, bool value) { this._structure.Add(key, value); @@ -64,9 +85,15 @@ public EvaluationContext Add(string key, bool value) /// /// Add a new string Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, string value) { this._structure.Add(key, value); @@ -76,9 +103,15 @@ public EvaluationContext Add(string key, string value) /// /// Add a new int Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, int value) { this._structure.Add(key, value); @@ -88,9 +121,15 @@ public EvaluationContext Add(string key, int value) /// /// Add a new double Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, double value) { this._structure.Add(key, value); @@ -100,9 +139,15 @@ public EvaluationContext Add(string key, double value) /// /// Add a new DateTime Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, DateTime value) { this._structure.Add(key, value); @@ -112,9 +157,15 @@ public EvaluationContext Add(string key, DateTime value) /// /// Add a new Structure Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, Structure value) { this._structure.Add(key, value); @@ -124,9 +175,15 @@ public EvaluationContext Add(string key, Structure value) /// /// Add a new List Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, List value) { this._structure.Add(key, value); @@ -136,9 +193,15 @@ public EvaluationContext Add(string key, List value) /// /// Add a new Value to the evaluation context /// - /// - /// - /// + /// The key of the value to be added + /// The value to be added + /// This + /// + /// Thrown when the key is . + /// + /// + /// Thrown when an element with the same key is already contained in the context. + /// public EvaluationContext Add(string key, Value value) { this._structure.Add(key, value); @@ -173,7 +236,7 @@ public void Merge(EvaluationContext other) /// /// Return an enumerator for all values /// - /// + /// An enumerator for all values public IEnumerator> GetEnumerator() { return this._structure.GetEnumerator(); From 294bb70a817f09cb138667887938b565e1fb3a13 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 13 Sep 2022 12:07:40 -0700 Subject: [PATCH 028/316] Remove periods on doc comments. Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- .../Model/EvaluationContext.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/OpenFeature.SDK/Model/EvaluationContext.cs b/src/OpenFeature.SDK/Model/EvaluationContext.cs index 276681b8..cd73b2cc 100644 --- a/src/OpenFeature.SDK/Model/EvaluationContext.cs +++ b/src/OpenFeature.SDK/Model/EvaluationContext.cs @@ -16,12 +16,12 @@ public class EvaluationContext /// Gets the Value at the specified key /// /// The key of the value to be retrieved - /// The associated with the key. + /// The associated with the key /// - /// Thrown when the context does not contain the specified key. + /// Thrown when the context does not contain the specified key /// /// - /// Thrown when the key is . + /// Thrown when the key is /// public Value GetValue(string key) => this._structure.GetValue(key); @@ -29,9 +29,9 @@ public class EvaluationContext /// 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. + /// indicating the presence of the key /// - /// Thrown when the key is . + /// Thrown when the key is /// public bool ContainsKey(string key) => this._structure.ContainsKey(key); @@ -40,18 +40,18 @@ public class EvaluationContext /// /// The key of the value to be removed /// - /// Thrown when the key is . + /// Thrown when the key is /// public void Remove(string key) => this._structure.Remove(key); /// /// Gets the value associated with the specified key /// - /// The or if the key was not present. + /// The or if the key was not present /// The key of the value to be retrieved - /// indicating the presence of the key. + /// indicating the presence of the key /// - /// Thrown when the key is . + /// Thrown when the key is /// public bool TryGetValue(string key, out Value value) => this._structure.TryGetValue(key, out value); @@ -71,10 +71,10 @@ public IDictionary AsDictionary() /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, bool value) { @@ -89,10 +89,10 @@ public EvaluationContext Add(string key, bool value) /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, string value) { @@ -107,10 +107,10 @@ public EvaluationContext Add(string key, string value) /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, int value) { @@ -125,10 +125,10 @@ public EvaluationContext Add(string key, int value) /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, double value) { @@ -143,10 +143,10 @@ public EvaluationContext Add(string key, double value) /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, DateTime value) { @@ -161,10 +161,10 @@ public EvaluationContext Add(string key, DateTime value) /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, Structure value) { @@ -179,10 +179,10 @@ public EvaluationContext Add(string key, Structure value) /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, List value) { @@ -197,10 +197,10 @@ public EvaluationContext Add(string key, List value) /// The value to be added /// This /// - /// Thrown when the key is . + /// Thrown when the key is /// /// - /// Thrown when an element with the same key is already contained in the context. + /// Thrown when an element with the same key is already contained in the context /// public EvaluationContext Add(string key, Value value) { From 77dd5f75d767385481f768728f49f53f9fa7342a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:39:33 +0000 Subject: [PATCH 029/316] Chore(deps): Bump codecov/codecov-action from 3.1.0 to 3.1.1 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.0...v3.1.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .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 363091bb..07742a62 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -35,7 +35,7 @@ jobs: - name: Test ${{ matrix.version }} run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v3.1.0 + - uses: codecov/codecov-action@v3.1.1 with: env_vars: OS name: Code Coverage for ${{ matrix.os }} From b008b76aa72fdd8f2d43b56808aea3842675db95 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:22:06 +1000 Subject: [PATCH 030/316] chore: use release please Standardise the release/development experience across the openfeature sdks by using release-please. - Remove usage of minver - Remove dotnet releaser - Setup release please and use simple type - Add action to lint commit messages Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .config/dotnet-tools.json | 6 ---- .github/workflows/lint-pr.yml | 17 +++++++++++ .github/workflows/release-package.yml | 21 ------------- .github/workflows/release.yml | 43 +++++++++++++++++++++++++++ .gitignore | 3 -- .release-please-manifest.json | 3 ++ OpenFeature.SDK.sln | 5 ++-- build/Common.prod.props | 16 +++------- build/Common.props | 1 - build/RELEASING.md | 9 ------ build/dotnet-releaser.toml | 22 -------------- release-please-config.json | 13 ++++++++ 12 files changed, 82 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/lint-pr.yml delete mode 100644 .github/workflows/release-package.yml create mode 100644 .github/workflows/release.yml create mode 100644 .release-please-manifest.json delete mode 100644 build/RELEASING.md delete mode 100644 build/dotnet-releaser.toml create mode 100644 release-please-config.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 494c6898..a02f5dc5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,12 +7,6 @@ "commands": [ "dotnet-format" ] - }, - "dotnet-releaser": { - "version": "0.4.2", - "commands": [ - "dotnet-releaser" - ] } } } diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml new file mode 100644 index 00000000..cd91d143 --- /dev/null +++ b/.github/workflows/lint-pr.yml @@ -0,0 +1,17 @@ +name: 'Lint PR' + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml deleted file mode 100644 index a8199e32..00000000 --- a/.github/workflows/release-package.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Package - -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - -jobs: - release-package: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Restore Tools - run: dotnet tool restore - - - name: Build, Tests, Pack and Publish - run: dotnet releaser run --nuget-token "${{secrets.NUGET_TOKEN}}" --github-token "${{secrets.GITHUB_TOKEN}}" build/dotnet-releaser.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..a93c29f7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: Run Release Release + +on: + push: + branches: + - main + +jobs: + release-package: + runs-on: windows-latest + + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + command: manifest + token: ${{secrets.GITHUB_TOKEN}} + default-branch: main + + - uses: actions/checkout@v3 + if: ${{ steps.release.outputs.releases_created }} + with: + fetch-depth: 0 + + - name: Install dependencies + if: ${{ steps.release.outputs.releases_created }} + run: dotnet restore + + - name: Build + if: ${{ steps.release.outputs.releases_created }} + run: | + dotnet build --configuration Release --no-restore -p:Deterministic=true + + - name: Pack + if: ${{ steps.release.outputs.releases_created }} + run: | + dotnet pack OpenFeature.SDK.proj --configuration Release --no-build -p:PackageID=OpenFeature + + - name: Publish to Nuget + if: ${{ steps.release.outputs.releases_created }} + run: | + VERSION=${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} + dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json diff --git a/.gitignore b/.gitignore index c021583e..cb35ed59 100644 --- a/.gitignore +++ b/.gitignore @@ -348,6 +348,3 @@ ASALocalRun/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json - -# dotnet releaser artifacts -/build/artifacts-dotnet-releaser diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..ab67d0fc --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.5" +} \ No newline at end of file diff --git a/OpenFeature.SDK.sln b/OpenFeature.SDK.sln index d1986dc1..11571a55 100644 --- a/OpenFeature.SDK.sln +++ b/OpenFeature.SDK.sln @@ -7,16 +7,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60 build\Common.props = build\Common.props build\Common.tests.props = build\Common.tests.props build\Common.prod.props = build\Common.prod.props - build\RELEASING.md = build\RELEASING.md .github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml .github\workflows\dotnet-format.yml = .github\workflows\dotnet-format.yml .github\workflows\linux-ci.yml = .github\workflows\linux-ci.yml - .github\workflows\release-package.yml = .github\workflows\release-package.yml + .github\workflows\release.yml = .github\workflows\release.yml .github\workflows\windows-ci.yml = .github\workflows\windows-ci.yml README.md = README.md CONTRIBUTING.md = CONTRIBUTING.md - build\dotnet-releaser.toml = build\dotnet-releaser.toml + .github\workflows\lint-pr.yml = .github\workflows\lint-pr.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C97E9975-E10A-4817-AE2C-4DD69C3C02D4}" diff --git a/build/Common.prod.props b/build/Common.prod.props index b440beb3..bc0262d7 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -1,23 +1,12 @@ - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - true - v - 0.1 - - - + 0.1.5 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. @@ -28,6 +17,9 @@ Apache-2.0 OpenFeature Authors true + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) diff --git a/build/Common.props b/build/Common.props index fa9b7e2a..aae4ba90 100644 --- a/build/Common.props +++ b/build/Common.props @@ -18,7 +18,6 @@ Please sort alphabetically. Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax. --> - [4.1.0,5.0) [2.0,6.0) [1.0.0,2.0) diff --git a/build/RELEASING.md b/build/RELEASING.md deleted file mode 100644 index be29dbd3..00000000 --- a/build/RELEASING.md +++ /dev/null @@ -1,9 +0,0 @@ -ο»Ώ# Release process - -Only for release managers - -1. Decide on the version name to be released. e.g. 0.1.0, 0.1.1 etc -2. Create tag via github ui -3. Release package action will fire on tag creation that will build, pack and publish and create the github release - - diff --git a/build/dotnet-releaser.toml b/build/dotnet-releaser.toml deleted file mode 100644 index a4ebcb18..00000000 --- a/build/dotnet-releaser.toml +++ /dev/null @@ -1,22 +0,0 @@ -# configuration file for dotnet-releaser -[msbuild] -project = "../OpenFeature.SDK.sln" -configuration = "Release" -[msbuild.properties] -Deterministic = true -[github] -user = "open-feature" -repo = "dotnet-sdk" -version_prefix = "v" -[test] -enable = false -[coverage] -enable = false -[coveralls] -publish = false -[brew] -publish = false -[service] -publish = false -[changelog] -publish = true diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..fba24092 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,13 @@ +{ + "packages": { + ".": { + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "./build/Common.prod.props" + ] + } + } +} \ No newline at end of file From 430ffc0a3afc871772286241d39a613c91298da5 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 23 Sep 2022 07:39:15 +1000 Subject: [PATCH 031/316] fix!: Rename namespace from OpenFeature.SDK to OpenFeatureSDK (#62) --- .github/workflows/release.yml | 2 +- OpenFeature.SDK.proj => OpenFeatureSDK.proj | 0 OpenFeature.SDK.sln => OpenFeatureSDK.sln | 4 ++-- README.md | 6 +++--- build/Common.tests.props | 2 +- src/Directory.Build.props | 2 +- .../Constant/ErrorType.cs | 2 +- .../Constant/FlagValueType.cs | 2 +- .../Constant/NoOpProvider.cs | 2 +- .../Constant/Reason.cs | 2 +- .../Error/FeatureProviderException.cs | 6 +++--- .../Extension/EnumExtensions.cs | 2 +- .../Extension/ResolutionDetailsExtensions.cs | 4 ++-- .../FeatureProvider.cs | 4 ++-- src/{OpenFeature.SDK => OpenFeatureSDK}/Hook.cs | 4 ++-- .../IFeatureClient.cs | 4 ++-- .../Model/ClientMetadata.cs | 2 +- .../Model/EvaluationContext.cs | 2 +- .../Model/FlagEvaluationOptions.cs | 2 +- .../Model/FlagEvalusationDetails.cs | 6 +++--- .../Model/HookContext.cs | 4 ++-- .../Model/Metadata.cs | 2 +- .../Model/ResolutionDetails.cs | 4 ++-- .../Model/Structure.cs | 2 +- .../Model/Value.cs | 2 +- .../NoOpProvider.cs | 6 +++--- .../OpenFeature.cs | 4 ++-- .../OpenFeatureClient.cs | 10 +++++----- .../OpenFeatureSDK.csproj} | 4 ++-- test/Directory.Build.props | 2 +- .../ClearOpenFeatureInstanceFixture.cs | 2 +- .../FeatureProviderExceptionTests.cs | 6 +++--- .../FeatureProviderTests.cs | 8 ++++---- .../Internal/SpecificationAttribute.cs | 2 +- .../OpenFeatureClientTests.cs | 12 ++++++------ .../OpenFeatureEvaluationContextTests.cs | 6 +++--- .../OpenFeatureHookTests.cs | 8 ++++---- .../OpenFeatureSDK.Tests.csproj} | 4 ++-- .../OpenFeatureTests.cs | 8 ++++---- .../StructureTests.cs | 4 ++-- .../TestImplementations.cs | 4 ++-- .../ValueTests.cs | 4 ++-- 42 files changed, 84 insertions(+), 84 deletions(-) rename OpenFeature.SDK.proj => OpenFeatureSDK.proj (100%) rename OpenFeature.SDK.sln => OpenFeatureSDK.sln (93%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Constant/ErrorType.cs (93%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Constant/FlagValueType.cs (89%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Constant/NoOpProvider.cs (83%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Constant/Reason.cs (94%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Error/FeatureProviderException.cs (92%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Extension/EnumExtensions.cs (89%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Extension/ResolutionDetailsExtensions.cs (81%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/FeatureProvider.cs (96%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Hook.cs (96%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/IFeatureClient.cs (95%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/ClientMetadata.cs (91%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/EvaluationContext.cs (97%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/FlagEvaluationOptions.cs (95%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/FlagEvalusationDetails.cs (93%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/HookContext.cs (95%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/Metadata.cs (91%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/ResolutionDetails.cs (94%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/Structure.cs (96%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/Model/Value.cs (97%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/NoOpProvider.cs (92%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/OpenFeature.cs (96%) rename src/{OpenFeature.SDK => OpenFeatureSDK}/OpenFeatureClient.cs (97%) rename src/{OpenFeature.SDK/OpenFeature.SDK.csproj => OpenFeatureSDK/OpenFeatureSDK.csproj} (79%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/ClearOpenFeatureInstanceFixture.cs (89%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/FeatureProviderExceptionTests.cs (91%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/FeatureProviderTests.cs (97%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/Internal/SpecificationAttribute.cs (87%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/OpenFeatureClientTests.cs (97%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/OpenFeatureEvaluationContextTests.cs (95%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/OpenFeatureHookTests.cs (97%) rename test/{OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj => OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj} (91%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/OpenFeatureTests.cs (94%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/StructureTests.cs (95%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/TestImplementations.cs (95%) rename test/{OpenFeature.SDK.Tests => OpenFeatureSDK.Tests}/ValueTests.cs (95%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a93c29f7..c38b3823 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: - name: Pack if: ${{ steps.release.outputs.releases_created }} run: | - dotnet pack OpenFeature.SDK.proj --configuration Release --no-build -p:PackageID=OpenFeature + dotnet pack OpenFeatureSDK.proj --configuration Release --no-build -p:PackageID=OpenFeature - name: Publish to Nuget if: ${{ steps.release.outputs.releases_created }} diff --git a/OpenFeature.SDK.proj b/OpenFeatureSDK.proj similarity index 100% rename from OpenFeature.SDK.proj rename to OpenFeatureSDK.proj diff --git a/OpenFeature.SDK.sln b/OpenFeatureSDK.sln similarity index 93% rename from OpenFeature.SDK.sln rename to OpenFeatureSDK.sln index 11571a55..064f140e 100644 --- a/OpenFeature.SDK.sln +++ b/OpenFeatureSDK.sln @@ -1,6 +1,6 @@ ο»Ώ Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.SDK", "src\OpenFeature.SDK\OpenFeature.SDK.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeatureSDK", "src\OpenFeatureSDK\OpenFeatureSDK.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}" ProjectSection(SolutionItems) = preProject @@ -29,7 +29,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{65FBA159-2 test\Directory.Build.props = test\Directory.Build.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.SDK.Tests", "test\OpenFeature.SDK.Tests\OpenFeature.SDK.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeatureSDK.Tests", "test\OpenFeatureSDK.Tests\OpenFeatureSDK.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index 852163e8..41a43ed4 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The packages will aim to support all current .NET versions. Refer to the current ### Basic Usage ```csharp -using OpenFeature.SDK; +using OpenFeatureSDK; // Sets the provider used by the client OpenFeature.Instance.SetProvider(new NoOpProvider()); @@ -40,8 +40,8 @@ To develop a provider, you need to create a new project and include the OpenFeat Example of implementing a feature flag provider ```csharp -using OpenFeature.SDK; -using OpenFeature.SDK.Model; +using OpenFeatureSDK; +using OpenFeatureSDK.Model; public class MyFeatureProvider : FeatureProvider { diff --git a/build/Common.tests.props b/build/Common.tests.props index 4cdbe034..677a51ec 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -10,7 +10,7 @@ - + PreserveNewest diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 157b717e..47c2b439 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,3 +1,3 @@ - + diff --git a/src/OpenFeature.SDK/Constant/ErrorType.cs b/src/OpenFeatureSDK/Constant/ErrorType.cs similarity index 93% rename from src/OpenFeature.SDK/Constant/ErrorType.cs rename to src/OpenFeatureSDK/Constant/ErrorType.cs index 936db1f6..e1dee893 100644 --- a/src/OpenFeature.SDK/Constant/ErrorType.cs +++ b/src/OpenFeatureSDK/Constant/ErrorType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace OpenFeature.SDK.Constant +namespace OpenFeatureSDK.Constant { /// /// These errors are used to indicate abnormal execution when evaluation a flag diff --git a/src/OpenFeature.SDK/Constant/FlagValueType.cs b/src/OpenFeatureSDK/Constant/FlagValueType.cs similarity index 89% rename from src/OpenFeature.SDK/Constant/FlagValueType.cs rename to src/OpenFeatureSDK/Constant/FlagValueType.cs index 26b29921..5eb328cd 100644 --- a/src/OpenFeature.SDK/Constant/FlagValueType.cs +++ b/src/OpenFeatureSDK/Constant/FlagValueType.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.SDK.Constant +namespace OpenFeatureSDK.Constant { /// /// Used to identity what object type of flag being evaluated diff --git a/src/OpenFeature.SDK/Constant/NoOpProvider.cs b/src/OpenFeatureSDK/Constant/NoOpProvider.cs similarity index 83% rename from src/OpenFeature.SDK/Constant/NoOpProvider.cs rename to src/OpenFeatureSDK/Constant/NoOpProvider.cs index 210ac24d..e3f8eee5 100644 --- a/src/OpenFeature.SDK/Constant/NoOpProvider.cs +++ b/src/OpenFeatureSDK/Constant/NoOpProvider.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.SDK.Constant +namespace OpenFeatureSDK.Constant { internal static class NoOpProvider { diff --git a/src/OpenFeature.SDK/Constant/Reason.cs b/src/OpenFeatureSDK/Constant/Reason.cs similarity index 94% rename from src/OpenFeature.SDK/Constant/Reason.cs rename to src/OpenFeatureSDK/Constant/Reason.cs index 70d0c177..81729809 100644 --- a/src/OpenFeature.SDK/Constant/Reason.cs +++ b/src/OpenFeatureSDK/Constant/Reason.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.SDK.Constant +namespace OpenFeatureSDK.Constant { /// /// Common reasons used during flag resolution diff --git a/src/OpenFeature.SDK/Error/FeatureProviderException.cs b/src/OpenFeatureSDK/Error/FeatureProviderException.cs similarity index 92% rename from src/OpenFeature.SDK/Error/FeatureProviderException.cs rename to src/OpenFeatureSDK/Error/FeatureProviderException.cs index dc3368bb..3e1d4ca8 100644 --- a/src/OpenFeature.SDK/Error/FeatureProviderException.cs +++ b/src/OpenFeatureSDK/Error/FeatureProviderException.cs @@ -1,8 +1,8 @@ using System; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Extension; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Extension; -namespace OpenFeature.SDK.Error +namespace OpenFeatureSDK.Error { /// /// Used to represent an abnormal error when evaluating a flag. This exception should be thrown diff --git a/src/OpenFeature.SDK/Extension/EnumExtensions.cs b/src/OpenFeatureSDK/Extension/EnumExtensions.cs similarity index 89% rename from src/OpenFeature.SDK/Extension/EnumExtensions.cs rename to src/OpenFeatureSDK/Extension/EnumExtensions.cs index 2647e383..2094486c 100644 --- a/src/OpenFeature.SDK/Extension/EnumExtensions.cs +++ b/src/OpenFeatureSDK/Extension/EnumExtensions.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Linq; -namespace OpenFeature.SDK.Extension +namespace OpenFeatureSDK.Extension { internal static class EnumExtensions { diff --git a/src/OpenFeature.SDK/Extension/ResolutionDetailsExtensions.cs b/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs similarity index 81% rename from src/OpenFeature.SDK/Extension/ResolutionDetailsExtensions.cs rename to src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs index 823c6c3b..72e53154 100644 --- a/src/OpenFeature.SDK/Extension/ResolutionDetailsExtensions.cs +++ b/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs @@ -1,6 +1,6 @@ -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK.Extension +namespace OpenFeatureSDK.Extension { internal static class ResolutionDetailsExtensions { diff --git a/src/OpenFeature.SDK/FeatureProvider.cs b/src/OpenFeatureSDK/FeatureProvider.cs similarity index 96% rename from src/OpenFeature.SDK/FeatureProvider.cs rename to src/OpenFeatureSDK/FeatureProvider.cs index 648f5db1..639363a8 100644 --- a/src/OpenFeature.SDK/FeatureProvider.cs +++ b/src/OpenFeatureSDK/FeatureProvider.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK +namespace OpenFeatureSDK { /// /// The provider interface describes the abstraction layer for a feature flag provider. diff --git a/src/OpenFeature.SDK/Hook.cs b/src/OpenFeatureSDK/Hook.cs similarity index 96% rename from src/OpenFeature.SDK/Hook.cs rename to src/OpenFeatureSDK/Hook.cs index 7159cae0..5a478e20 100644 --- a/src/OpenFeature.SDK/Hook.cs +++ b/src/OpenFeatureSDK/Hook.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK +namespace OpenFeatureSDK { /// /// The Hook abstract class describes the default implementation for a hook. diff --git a/src/OpenFeature.SDK/IFeatureClient.cs b/src/OpenFeatureSDK/IFeatureClient.cs similarity index 95% rename from src/OpenFeature.SDK/IFeatureClient.cs rename to src/OpenFeatureSDK/IFeatureClient.cs index 99af15c3..9c1fdfcc 100644 --- a/src/OpenFeature.SDK/IFeatureClient.cs +++ b/src/OpenFeatureSDK/IFeatureClient.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Threading.Tasks; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK +namespace OpenFeatureSDK { internal interface IFeatureClient { diff --git a/src/OpenFeature.SDK/Model/ClientMetadata.cs b/src/OpenFeatureSDK/Model/ClientMetadata.cs similarity index 91% rename from src/OpenFeature.SDK/Model/ClientMetadata.cs rename to src/OpenFeatureSDK/Model/ClientMetadata.cs index 7a502a6a..bfe80895 100644 --- a/src/OpenFeature.SDK/Model/ClientMetadata.cs +++ b/src/OpenFeatureSDK/Model/ClientMetadata.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// Represents the client metadata diff --git a/src/OpenFeature.SDK/Model/EvaluationContext.cs b/src/OpenFeatureSDK/Model/EvaluationContext.cs similarity index 97% rename from src/OpenFeature.SDK/Model/EvaluationContext.cs rename to src/OpenFeatureSDK/Model/EvaluationContext.cs index cd73b2cc..c50561e6 100644 --- a/src/OpenFeature.SDK/Model/EvaluationContext.cs +++ b/src/OpenFeatureSDK/Model/EvaluationContext.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// A KeyValuePair with a string key and object value that is used to apply user defined properties diff --git a/src/OpenFeature.SDK/Model/FlagEvaluationOptions.cs b/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs similarity index 95% rename from src/OpenFeature.SDK/Model/FlagEvaluationOptions.cs rename to src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs index 53aacceb..cb8bc353 100644 --- a/src/OpenFeature.SDK/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// A structure containing the one or more hooks and hook hints diff --git a/src/OpenFeature.SDK/Model/FlagEvalusationDetails.cs b/src/OpenFeatureSDK/Model/FlagEvalusationDetails.cs similarity index 93% rename from src/OpenFeature.SDK/Model/FlagEvalusationDetails.cs rename to src/OpenFeatureSDK/Model/FlagEvalusationDetails.cs index 2b133bd6..f1c4d125 100644 --- a/src/OpenFeature.SDK/Model/FlagEvalusationDetails.cs +++ b/src/OpenFeatureSDK/Model/FlagEvalusationDetails.cs @@ -1,7 +1,7 @@ -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Extension; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Extension; -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// The contract returned to the caller that describes the result of the flag evaluation process. diff --git a/src/OpenFeature.SDK/Model/HookContext.cs b/src/OpenFeatureSDK/Model/HookContext.cs similarity index 95% rename from src/OpenFeature.SDK/Model/HookContext.cs rename to src/OpenFeatureSDK/Model/HookContext.cs index 7a610272..d97b8333 100644 --- a/src/OpenFeature.SDK/Model/HookContext.cs +++ b/src/OpenFeatureSDK/Model/HookContext.cs @@ -1,7 +1,7 @@ using System; -using OpenFeature.SDK.Constant; +using OpenFeatureSDK.Constant; -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// Context provided to hook execution diff --git a/src/OpenFeature.SDK/Model/Metadata.cs b/src/OpenFeatureSDK/Model/Metadata.cs similarity index 91% rename from src/OpenFeature.SDK/Model/Metadata.cs rename to src/OpenFeatureSDK/Model/Metadata.cs index ebbd43ed..9228b462 100644 --- a/src/OpenFeature.SDK/Model/Metadata.cs +++ b/src/OpenFeatureSDK/Model/Metadata.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// metadata diff --git a/src/OpenFeature.SDK/Model/ResolutionDetails.cs b/src/OpenFeatureSDK/Model/ResolutionDetails.cs similarity index 94% rename from src/OpenFeature.SDK/Model/ResolutionDetails.cs rename to src/OpenFeatureSDK/Model/ResolutionDetails.cs index 4672327f..85ba8d35 100644 --- a/src/OpenFeature.SDK/Model/ResolutionDetails.cs +++ b/src/OpenFeatureSDK/Model/ResolutionDetails.cs @@ -1,6 +1,6 @@ -using OpenFeature.SDK.Constant; +using OpenFeatureSDK.Constant; -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// Defines the contract that the is required to return diff --git a/src/OpenFeature.SDK/Model/Structure.cs b/src/OpenFeatureSDK/Model/Structure.cs similarity index 96% rename from src/OpenFeature.SDK/Model/Structure.cs rename to src/OpenFeatureSDK/Model/Structure.cs index 508b4083..eec68240 100644 --- a/src/OpenFeature.SDK/Model/Structure.cs +++ b/src/OpenFeatureSDK/Model/Structure.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// Structure represents a map of Values diff --git a/src/OpenFeature.SDK/Model/Value.cs b/src/OpenFeatureSDK/Model/Value.cs similarity index 97% rename from src/OpenFeature.SDK/Model/Value.cs rename to src/OpenFeatureSDK/Model/Value.cs index 941eecba..308c698a 100644 --- a/src/OpenFeature.SDK/Model/Value.cs +++ b/src/OpenFeatureSDK/Model/Value.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; -namespace OpenFeature.SDK.Model +namespace OpenFeatureSDK.Model { /// /// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format. diff --git a/src/OpenFeature.SDK/NoOpProvider.cs b/src/OpenFeatureSDK/NoOpProvider.cs similarity index 92% rename from src/OpenFeature.SDK/NoOpProvider.cs rename to src/OpenFeatureSDK/NoOpProvider.cs index a406a994..94fa7c2e 100644 --- a/src/OpenFeature.SDK/NoOpProvider.cs +++ b/src/OpenFeatureSDK/NoOpProvider.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK +namespace OpenFeatureSDK { internal class NoOpFeatureProvider : FeatureProvider { diff --git a/src/OpenFeature.SDK/OpenFeature.cs b/src/OpenFeatureSDK/OpenFeature.cs similarity index 96% rename from src/OpenFeature.SDK/OpenFeature.cs rename to src/OpenFeatureSDK/OpenFeature.cs index 31079cd0..8ea95048 100644 --- a/src/OpenFeature.SDK/OpenFeature.cs +++ b/src/OpenFeatureSDK/OpenFeature.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK +namespace OpenFeatureSDK { /// /// The evaluation API allows for the evaluation of feature flag values, independent of any flag control plane or vendor. diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeatureSDK/OpenFeatureClient.cs similarity index 97% rename from src/OpenFeature.SDK/OpenFeatureClient.cs rename to src/OpenFeatureSDK/OpenFeatureClient.cs index 04125fec..f46bbd59 100644 --- a/src/OpenFeature.SDK/OpenFeatureClient.cs +++ b/src/OpenFeatureSDK/OpenFeatureClient.cs @@ -4,12 +4,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Error; -using OpenFeature.SDK.Extension; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Error; +using OpenFeatureSDK.Extension; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK +namespace OpenFeatureSDK { /// /// diff --git a/src/OpenFeature.SDK/OpenFeature.SDK.csproj b/src/OpenFeatureSDK/OpenFeatureSDK.csproj similarity index 79% rename from src/OpenFeature.SDK/OpenFeature.SDK.csproj rename to src/OpenFeatureSDK/OpenFeatureSDK.csproj index 4f479f01..3e564e99 100644 --- a/src/OpenFeature.SDK/OpenFeature.SDK.csproj +++ b/src/OpenFeatureSDK/OpenFeatureSDK.csproj @@ -2,7 +2,7 @@ netstandard2.0;net462 - OpenFeature.SDK + OpenFeatureSDK @@ -11,7 +11,7 @@ - <_Parameter1>OpenFeature.SDK.Tests + <_Parameter1>OpenFeatureSDK.Tests diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 3ed2d6f4..5e1104b8 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,3 +1,3 @@ - + diff --git a/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeatureSDK.Tests/ClearOpenFeatureInstanceFixture.cs similarity index 89% rename from test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs rename to test/OpenFeatureSDK.Tests/ClearOpenFeatureInstanceFixture.cs index 3cf4bb5d..b15b2c30 100644 --- a/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs +++ b/test/OpenFeatureSDK.Tests/ClearOpenFeatureInstanceFixture.cs @@ -1,4 +1,4 @@ -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class ClearOpenFeatureInstanceFixture { diff --git a/test/OpenFeature.SDK.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs similarity index 91% rename from test/OpenFeature.SDK.Tests/FeatureProviderExceptionTests.cs rename to test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs index 8d100176..ecdd5740 100644 --- a/test/OpenFeature.SDK.Tests/FeatureProviderExceptionTests.cs +++ b/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs @@ -1,10 +1,10 @@ using System; using FluentAssertions; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Error; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Error; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class FeatureProviderExceptionTests { diff --git a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs b/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs similarity index 97% rename from test/OpenFeature.SDK.Tests/FeatureProviderTests.cs rename to test/OpenFeatureSDK.Tests/FeatureProviderTests.cs index 8965eff0..ad524a3b 100644 --- a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs +++ b/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs @@ -2,12 +2,12 @@ using AutoFixture; using FluentAssertions; using Moq; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Model; -using OpenFeature.SDK.Tests.Internal; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Model; +using OpenFeatureSDK.Tests.Internal; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { diff --git a/test/OpenFeature.SDK.Tests/Internal/SpecificationAttribute.cs b/test/OpenFeatureSDK.Tests/Internal/SpecificationAttribute.cs similarity index 87% rename from test/OpenFeature.SDK.Tests/Internal/SpecificationAttribute.cs rename to test/OpenFeatureSDK.Tests/Internal/SpecificationAttribute.cs index 624c54cc..68a79ab3 100644 --- a/test/OpenFeature.SDK.Tests/Internal/SpecificationAttribute.cs +++ b/test/OpenFeatureSDK.Tests/Internal/SpecificationAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace OpenFeature.SDK.Tests.Internal +namespace OpenFeatureSDK.Tests.Internal { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] public class SpecificationAttribute : Attribute diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs similarity index 97% rename from test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs rename to test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs index 5902908e..705db1a9 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs @@ -5,14 +5,14 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Error; -using OpenFeature.SDK.Extension; -using OpenFeature.SDK.Model; -using OpenFeature.SDK.Tests.Internal; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Error; +using OpenFeatureSDK.Extension; +using OpenFeatureSDK.Model; +using OpenFeatureSDK.Tests.Internal; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture { diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs similarity index 95% rename from test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs rename to test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs index ff913c13..8c743568 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using AutoFixture; using FluentAssertions; -using OpenFeature.SDK.Model; -using OpenFeature.SDK.Tests.Internal; +using OpenFeatureSDK.Model; +using OpenFeatureSDK.Tests.Internal; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class OpenFeatureEvaluationContextTests { diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs similarity index 97% rename from test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs rename to test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs index 7b470be5..8266e2d7 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs @@ -4,12 +4,12 @@ using AutoFixture; using FluentAssertions; using Moq; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Model; -using OpenFeature.SDK.Tests.Internal; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Model; +using OpenFeatureSDK.Tests.Internal; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture { diff --git a/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj b/test/OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj similarity index 91% rename from test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj rename to test/OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj index f9765bb6..ec5b0216 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeature.SDK.Tests.csproj +++ b/test/OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj @@ -3,7 +3,7 @@ net6.0 $(TargetFrameworks);net462 - OpenFeature.SDK.Tests + OpenFeatureSDK.Tests @@ -27,7 +27,7 @@ - + diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs similarity index 94% rename from test/OpenFeature.SDK.Tests/OpenFeatureTests.cs rename to test/OpenFeatureSDK.Tests/OpenFeatureTests.cs index 03144afa..5150f032 100644 --- a/test/OpenFeature.SDK.Tests/OpenFeatureTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs @@ -1,12 +1,12 @@ using AutoFixture; using FluentAssertions; using Moq; -using OpenFeature.SDK.Constant; -using OpenFeature.SDK.Model; -using OpenFeature.SDK.Tests.Internal; +using OpenFeatureSDK.Constant; +using OpenFeatureSDK.Model; +using OpenFeatureSDK.Tests.Internal; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { diff --git a/test/OpenFeature.SDK.Tests/StructureTests.cs b/test/OpenFeatureSDK.Tests/StructureTests.cs similarity index 95% rename from test/OpenFeature.SDK.Tests/StructureTests.cs rename to test/OpenFeatureSDK.Tests/StructureTests.cs index e16757e8..2e78a62f 100644 --- a/test/OpenFeature.SDK.Tests/StructureTests.cs +++ b/test/OpenFeatureSDK.Tests/StructureTests.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class StructureTests { diff --git a/test/OpenFeature.SDK.Tests/TestImplementations.cs b/test/OpenFeatureSDK.Tests/TestImplementations.cs similarity index 95% rename from test/OpenFeature.SDK.Tests/TestImplementations.cs rename to test/OpenFeatureSDK.Tests/TestImplementations.cs index 1d0539aa..980aea01 100644 --- a/test/OpenFeature.SDK.Tests/TestImplementations.cs +++ b/test/OpenFeatureSDK.Tests/TestImplementations.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class TestHookNoOverride : Hook { } diff --git a/test/OpenFeature.SDK.Tests/ValueTests.cs b/test/OpenFeatureSDK.Tests/ValueTests.cs similarity index 95% rename from test/OpenFeature.SDK.Tests/ValueTests.cs rename to test/OpenFeatureSDK.Tests/ValueTests.cs index 47d4876a..cf3f214c 100644 --- a/test/OpenFeature.SDK.Tests/ValueTests.cs +++ b/test/OpenFeatureSDK.Tests/ValueTests.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using OpenFeature.SDK.Model; +using OpenFeatureSDK.Model; using Xunit; -namespace OpenFeature.SDK.Tests +namespace OpenFeatureSDK.Tests { public class ValueTests { From ee398399d9371517c4b03b55a93619776ecd3a92 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 23 Sep 2022 08:40:23 +1000 Subject: [PATCH 032/316] fix!: use correct path to extra file (#63) --- release-please-config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index fba24092..061b9cd7 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -6,8 +6,8 @@ "bump-patch-for-minor-pre-major": true, "versioning": "default", "extra-files": [ - "./build/Common.prod.props" + "build/Common.prod.props" ] } } -} \ No newline at end of file +} From 45a3d95fae5a550e92fe1b9beddd20f1c505f289 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 08:42:18 +1000 Subject: [PATCH 033/316] chore(main): release 0.2.0 (#64) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ build/Common.prod.props | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ab67d0fc..10f30916 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.5" + ".": "0.2.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5fe575be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## [0.2.0](https://github.com/open-feature/dotnet-sdk/compare/v0.1.5...v0.2.0) (2022-09-22) + + +### ⚠ BREAKING CHANGES + +* use correct path to extra file (#63) +* Rename namespace from OpenFeature.SDK to OpenFeatureSDK (#62) + +### Bug Fixes + +* Rename namespace from OpenFeature.SDK to OpenFeatureSDK ([#62](https://github.com/open-feature/dotnet-sdk/issues/62)) ([430ffc0](https://github.com/open-feature/dotnet-sdk/commit/430ffc0a3afc871772286241d39a613c91298da5)) +* use correct path to extra file ([#63](https://github.com/open-feature/dotnet-sdk/issues/63)) ([ee39839](https://github.com/open-feature/dotnet-sdk/commit/ee398399d9371517c4b03b55a93619776ecd3a92)) diff --git a/build/Common.prod.props b/build/Common.prod.props index bc0262d7..0342666f 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.1.5 + 0.2.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 8c8500c71edb84c256b177c40815a34607adb682 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 23 Sep 2022 09:09:40 +1000 Subject: [PATCH 034/316] fix: substitute version number into filename when pushing package (#65) --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c38b3823..40d0eb2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,5 +39,6 @@ jobs: - name: Publish to Nuget if: ${{ steps.release.outputs.releases_created }} run: | - VERSION=${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} - dotnet nuget push OpenFeature.{VERSION}.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json + dotnet nuget push OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg ` + --api-key ${{secrets.NUGET_API_KEY}} ` + --source https://api.nuget.org/v3/index.json From 63ecad04957a52b52e0be7fab32338b85dc3fc39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 09:12:00 +1000 Subject: [PATCH 035/316] chore(main): release 0.2.1 (#66) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ build/Common.prod.props | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f30916..b06ba919 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.2.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe575be..2405a0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.1](https://github.com/open-feature/dotnet-sdk/compare/v0.2.0...v0.2.1) (2022-09-22) + + +### Bug Fixes + +* substitute version number into filename when pushing package ([#65](https://github.com/open-feature/dotnet-sdk/issues/65)) ([8c8500c](https://github.com/open-feature/dotnet-sdk/commit/8c8500c71edb84c256b177c40815a34607adb682)) + ## [0.2.0](https://github.com/open-feature/dotnet-sdk/compare/v0.1.5...v0.2.0) (2022-09-22) diff --git a/build/Common.prod.props b/build/Common.prod.props index 0342666f..fdef1bbb 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.2.0 + 0.2.1 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 87c99b2128d50d72b54cb27e2f866f1edb0cd0d3 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 23 Sep 2022 09:21:00 +1000 Subject: [PATCH 036/316] fix: change NUGET_API_KEY -> NUGET_TOKEN (#67) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40d0eb2a..66dc697d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,5 +40,5 @@ jobs: if: ${{ steps.release.outputs.releases_created }} run: | dotnet nuget push OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg ` - --api-key ${{secrets.NUGET_API_KEY}} ` + --api-key ${{secrets.NUGET_TOKEN}} ` --source https://api.nuget.org/v3/index.json From e15ead595597ea99d0e148edbc769fdf5bba0ea0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 09:22:36 +1000 Subject: [PATCH 037/316] chore(main): release 0.2.2 (#68) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ build/Common.prod.props | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b06ba919..d66ca57c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.1" + ".": "0.2.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2405a0de..53238051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.2](https://github.com/open-feature/dotnet-sdk/compare/v0.2.1...v0.2.2) (2022-09-22) + + +### Bug Fixes + +* change NUGET_API_KEY -> NUGET_TOKEN ([#67](https://github.com/open-feature/dotnet-sdk/issues/67)) ([87c99b2](https://github.com/open-feature/dotnet-sdk/commit/87c99b2128d50d72b54cb27e2f866f1edb0cd0d3)) + ## [0.2.1](https://github.com/open-feature/dotnet-sdk/compare/v0.2.0...v0.2.1) (2022-09-22) diff --git a/build/Common.prod.props b/build/Common.prod.props index fdef1bbb..5565db50 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.2.1 + 0.2.2 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 6549dbb4f3a525a70cebdc9a63661ce6eaba9266 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 22 Sep 2022 19:42:43 -0400 Subject: [PATCH 038/316] fix: add dir to publish (#69) Signed-off-by: Todd Baert Signed-off-by: Todd Baert --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66dc697d..9dc750fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,6 @@ jobs: - name: Publish to Nuget if: ${{ steps.release.outputs.releases_created }} run: | - dotnet nuget push OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg ` + dotnet nuget push src/OpenFeatureSDK/bin/Release/OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg ` --api-key ${{secrets.NUGET_TOKEN}} ` --source https://api.nuget.org/v3/index.json From 3ee7d08b870da4a019858dfeca24200f8ea86366 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Sep 2022 19:43:51 -0400 Subject: [PATCH 039/316] chore(main): release 0.2.3 (#70) :robot: I have created a release *beep* *boop* --- ## [0.2.3](https://github.com/open-feature/dotnet-sdk/compare/v0.2.2...v0.2.3) (2022-09-22) ### Bug Fixes * add dir to publish ([#69](https://github.com/open-feature/dotnet-sdk/issues/69)) ([6549dbb](https://github.com/open-feature/dotnet-sdk/commit/6549dbb4f3a525a70cebdc9a63661ce6eaba9266)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ build/Common.prod.props | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d66ca57c..ccdf8aa7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.2" + ".": "0.2.3" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 53238051..561c15c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.3](https://github.com/open-feature/dotnet-sdk/compare/v0.2.2...v0.2.3) (2022-09-22) + + +### Bug Fixes + +* add dir to publish ([#69](https://github.com/open-feature/dotnet-sdk/issues/69)) ([6549dbb](https://github.com/open-feature/dotnet-sdk/commit/6549dbb4f3a525a70cebdc9a63661ce6eaba9266)) + ## [0.2.2](https://github.com/open-feature/dotnet-sdk/compare/v0.2.1...v0.2.2) (2022-09-22) diff --git a/build/Common.prod.props b/build/Common.prod.props index 5565db50..bc6a73c7 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.2.2 + 0.2.3 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 1f7e4cb622519a045351b053271c6cd64c395274 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 27 Sep 2022 08:08:04 -0400 Subject: [PATCH 040/316] chore: update link to ofep Signed-off-by: Michael Beemer --- .github/ISSUE_TEMPLATE/feature.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index 034e3d65..3833285d 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -9,6 +9,6 @@ body: description: | Ask us what you want! Please provide as many details as possible and describe how it should work. - Note: Spec and architecture changes require an [OFEP](https://github.com/open-feature/research). + Note: Spec and architecture changes require an [OFEP](https://github.com/open-feature/ofep). validations: required: false From e7ab49866bd83d7b146059b0c22944a7db6956b4 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 28 Sep 2022 11:19:56 -0700 Subject: [PATCH 041/316] feat!: ErrorType as enum, add ErrorMessage string (#72) --- src/OpenFeatureSDK/Constant/ErrorType.cs | 12 +- .../Error/FeatureProviderException.cs | 19 +-- .../Extension/ResolutionDetailsExtensions.cs | 3 +- ...ionDetails.cs => FlagEvaluationDetails.cs} | 34 ++--- src/OpenFeatureSDK/Model/ResolutionDetails.cs | 9 +- src/OpenFeatureSDK/OpenFeatureClient.cs | 6 +- .../FeatureProviderExceptionTests.cs | 11 +- .../FeatureProviderTests.cs | 132 +++++++++++++----- .../OpenFeatureClientTests.cs | 38 ++++- 9 files changed, 177 insertions(+), 87 deletions(-) rename src/OpenFeatureSDK/Model/{FlagEvalusationDetails.cs => FlagEvaluationDetails.cs} (69%) diff --git a/src/OpenFeatureSDK/Constant/ErrorType.cs b/src/OpenFeatureSDK/Constant/ErrorType.cs index e1dee893..f1b8464d 100644 --- a/src/OpenFeatureSDK/Constant/ErrorType.cs +++ b/src/OpenFeatureSDK/Constant/ErrorType.cs @@ -36,6 +36,16 @@ public enum ErrorType /// /// Abnormal execution of the provider /// - [Description("GENERAL")] General + [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, } } diff --git a/src/OpenFeatureSDK/Error/FeatureProviderException.cs b/src/OpenFeatureSDK/Error/FeatureProviderException.cs index 3e1d4ca8..6a4955c3 100644 --- a/src/OpenFeatureSDK/Error/FeatureProviderException.cs +++ b/src/OpenFeatureSDK/Error/FeatureProviderException.cs @@ -1,6 +1,5 @@ using System; using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Extension; namespace OpenFeatureSDK.Error { @@ -11,9 +10,9 @@ namespace OpenFeatureSDK.Error public class FeatureProviderException : Exception { /// - /// Description of error that occured when evaluating a flag + /// Error that occurred during evaluation /// - public string ErrorDescription { get; } + public ErrorType ErrorType { get; } /// /// Initialize a new instance of the class @@ -24,19 +23,7 @@ public class FeatureProviderException : Exception public FeatureProviderException(ErrorType errorType, string message = null, Exception innerException = null) : base(message, innerException) { - this.ErrorDescription = errorType.GetDescription(); - } - - /// - /// Initialize a new instance of the class - /// - /// A string representation describing the error that occured - /// Exception message - /// Optional inner exception - public FeatureProviderException(string errorCode, string message = null, Exception innerException = null) - : base(message, innerException) - { - this.ErrorDescription = errorCode; + this.ErrorType = errorType; } } } diff --git a/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs b/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs index 72e53154..1580e062 100644 --- a/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs +++ b/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs @@ -6,7 +6,8 @@ internal static class ResolutionDetailsExtensions { public static FlagEvaluationDetails ToFlagEvaluationDetails(this ResolutionDetails details) { - return new FlagEvaluationDetails(details.FlagKey, details.Value, details.ErrorType, details.Reason, details.Variant); + return new FlagEvaluationDetails(details.FlagKey, details.Value, details.ErrorType, details.Reason, + details.Variant, details.ErrorMessage); } } } diff --git a/src/OpenFeatureSDK/Model/FlagEvalusationDetails.cs b/src/OpenFeatureSDK/Model/FlagEvaluationDetails.cs similarity index 69% rename from src/OpenFeatureSDK/Model/FlagEvalusationDetails.cs rename to src/OpenFeatureSDK/Model/FlagEvaluationDetails.cs index f1c4d125..a70b90a5 100644 --- a/src/OpenFeatureSDK/Model/FlagEvalusationDetails.cs +++ b/src/OpenFeatureSDK/Model/FlagEvaluationDetails.cs @@ -1,5 +1,4 @@ using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Extension; namespace OpenFeatureSDK.Model { @@ -23,7 +22,16 @@ public class FlagEvaluationDetails /// /// Error that occurred during evaluation /// - public string ErrorType { get; } + 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; } /// /// Describes the reason for the outcome of the evaluation process @@ -45,30 +53,16 @@ public class FlagEvaluationDetails /// Error /// Reason /// Variant - public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string reason, string variant) - { - this.Value = value; - this.FlagKey = flagKey; - this.ErrorType = errorType.GetDescription(); - this.Reason = reason; - this.Variant = variant; - } - - /// - /// Initializes a new instance of the class. - /// - /// Feature flag key - /// Evaluated value - /// Error - /// Reason - /// Variant - public FlagEvaluationDetails(string flagKey, T value, string errorType, string reason, string variant) + /// Error message + public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string reason, string variant, + string errorMessage = null) { this.Value = value; this.FlagKey = flagKey; this.ErrorType = errorType; this.Reason = reason; this.Variant = variant; + this.ErrorMessage = errorMessage; } } } diff --git a/src/OpenFeatureSDK/Model/ResolutionDetails.cs b/src/OpenFeatureSDK/Model/ResolutionDetails.cs index 85ba8d35..b9f3d36a 100644 --- a/src/OpenFeatureSDK/Model/ResolutionDetails.cs +++ b/src/OpenFeatureSDK/Model/ResolutionDetails.cs @@ -26,6 +26,11 @@ public class ResolutionDetails /// public ErrorType ErrorType { get; } + /// + /// Message containing additional details about an error. + /// + public string ErrorMessage { get; } + /// /// Describes the reason for the outcome of the evaluation process /// @@ -47,14 +52,16 @@ public class ResolutionDetails /// Error /// Reason /// Variant + /// Error message public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string reason = null, - string variant = null) + string variant = null, string errorMessage = null) { this.Value = value; this.FlagKey = flagKey; this.ErrorType = errorType; this.Reason = reason; this.Variant = variant; + this.ErrorMessage = errorMessage; } } } diff --git a/src/OpenFeatureSDK/OpenFeatureClient.cs b/src/OpenFeatureSDK/OpenFeatureClient.cs index f46bbd59..a105644a 100644 --- a/src/OpenFeatureSDK/OpenFeatureClient.cs +++ b/src/OpenFeatureSDK/OpenFeatureClient.cs @@ -257,9 +257,9 @@ private async Task> EvaluateFlag( catch (FeatureProviderException ex) { this._logger.LogError(ex, "Error while evaluating flag {FlagKey}. Error {ErrorType}", flagKey, - ex.ErrorDescription); - evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorDescription, Reason.Error, - string.Empty); + ex.ErrorType.GetDescription()); + evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error, + string.Empty, ex.Message); await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options); } catch (Exception ex) diff --git a/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs index ecdd5740..5506af38 100644 --- a/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs +++ b/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs @@ -2,6 +2,7 @@ using FluentAssertions; using OpenFeatureSDK.Constant; using OpenFeatureSDK.Error; +using OpenFeatureSDK.Extension; using Xunit; namespace OpenFeatureSDK.Tests @@ -17,16 +18,16 @@ public class FeatureProviderExceptionTests public void FeatureProviderException_Should_Resolve_Description(ErrorType errorType, string errorDescription) { var ex = new FeatureProviderException(errorType); - ex.ErrorDescription.Should().Be(errorDescription); + ex.ErrorType.GetDescription().Should().Be(errorDescription); } [Theory] - [InlineData("OUT_OF_CREDIT", "Subscription has expired, please renew your subscription.")] - [InlineData("Exceed quota", "User has exceeded the quota for this feature.")] - public void FeatureProviderException_Should_Allow_Custom_ErrorCode_Messages(string errorCode, string message) + [InlineData(ErrorType.General, "Subscription has expired, please renew your subscription.")] + [InlineData(ErrorType.ProviderNotReady, "User has exceeded the quota for this feature.")] + public void FeatureProviderException_Should_Allow_Custom_ErrorCode_Messages(ErrorType errorCode, string message) { var ex = new FeatureProviderException(errorCode, message, new ArgumentOutOfRangeException("flag")); - ex.ErrorDescription.Should().Be(errorCode); + ex.ErrorType.Should().Be(errorCode); ex.Message.Should().Be(message); ex.InnerException.Should().BeOfType(); } diff --git a/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs b/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs index ad524a3b..37a62877 100644 --- a/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs +++ b/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs @@ -12,7 +12,8 @@ namespace OpenFeatureSDK.Tests public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { [Fact] - [Specification("2.1", "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")] + [Specification("2.1", + "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")] public void Provider_Must_Have_Metadata() { var provider = new TestProvider(); @@ -21,13 +22,22 @@ public void Provider_Must_Have_Metadata() } [Fact] - [Specification("2.2", "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `flag resolution` structure.")] - [Specification("2.3.1", "The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.")] - [Specification("2.4", "In cases of normal execution, the `provider` MUST populate the `flag resolution` structure's `value` field with the resolved flag value.")] - [Specification("2.5", "In cases of normal execution, the `provider` SHOULD populate the `flag resolution` structure's `variant` field with a string identifier corresponding to the returned flag value.")] - [Specification("2.6", "The `provider` SHOULD populate the `flag resolution` structure's `reason` field with a string indicating the semantic reason for the returned flag value.")] - [Specification("2.7", "In cases of normal execution, the `provider` MUST NOT populate the `flag resolution` structure's `error code` field, or otherwise must populate it with a null or falsy value.")] - [Specification("2.9", "In cases of normal execution, the `provider` MUST NOT populate the `flag resolution` structure's `error code` field, or otherwise must populate it with a null or falsy value.")] + [Specification("2.2", + "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `flag resolution` structure.")] + [Specification("2.3.1", + "The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.")] + [Specification("2.4", + "In cases of normal execution, the `provider` MUST populate the `flag resolution` structure's `value` field with the resolved flag value.")] + [Specification("2.5", + "In cases of normal execution, the `provider` SHOULD populate the `flag resolution` structure's `variant` field with a string identifier corresponding to the returned flag value.")] + [Specification("2.6", + "The `provider` SHOULD populate the `flag resolution` structure's `reason` field with a string indicating the semantic reason for the returned flag value.")] + [Specification("2.7", + "In cases of normal execution, the `provider` MUST NOT populate the `flag resolution` structure's `error code` field, or otherwise must populate it with a null or falsy value.")] + [Specification("2.9.1", + "The `flag resolution` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] + [Specification("2.11", + "In cases of normal execution, the `provider` MUST NOT populate the `flag resolution` structure's `error message` field, or otherwise must populate it with a null or falsy value.")] public async Task Provider_Must_Resolve_Flag_Values() { var fixture = new Fixture(); @@ -39,24 +49,37 @@ public async Task Provider_Must_Resolve_Flag_Values() var defaultStructureValue = fixture.Create(); var provider = new NoOpFeatureProvider(); - var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveBooleanValue(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolResolutionDetails); - - var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveIntegerValue(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerResolutionDetails); - - var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveDoubleValue(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleResolutionDetails); - - var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStringValue(flagName, defaultStringValue)).Should().BeEquivalentTo(stringResolutionDetails); - - var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStructureValue(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureResolutionDetails); + var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await provider.ResolveBooleanValue(flagName, defaultBoolValue)).Should() + .BeEquivalentTo(boolResolutionDetails); + + var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await provider.ResolveIntegerValue(flagName, defaultIntegerValue)).Should() + .BeEquivalentTo(integerResolutionDetails); + + var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await provider.ResolveDoubleValue(flagName, defaultDoubleValue)).Should() + .BeEquivalentTo(doubleResolutionDetails); + + var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await provider.ResolveStringValue(flagName, defaultStringValue)).Should() + .BeEquivalentTo(stringResolutionDetails); + + var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, + ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + (await provider.ResolveStructureValue(flagName, defaultStructureValue)).Should() + .BeEquivalentTo(structureResolutionDetails); } [Fact] - [Specification("2.8", "In cases of abnormal execution, the `provider` MUST indicate an error using the idioms of the implementation language, with an associated error code having possible values `PROVIDER_NOT_READY`, `FLAG_NOT_FOUND`, `PARSE_ERROR`, `TYPE_MISMATCH`, or `GENERAL`.")] + [Specification("2.8", + "In cases of abnormal execution, the `provider` MUST indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")] + [Specification("2.12", + "In cases of abnormal execution, the `evaluation details` structure's `error message` field MAY contain a string containing additional detail about the nature of the error.")] public async Task Provider_Must_ErrorType() { var fixture = new Fixture(); @@ -68,33 +91,68 @@ public async Task Provider_Must_ErrorType() var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); var providerMock = new Mock(MockBehavior.Strict); + const string testMessage = "An error message"; providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); providerMock.Setup(x => x.ResolveIntegerValue(flagName, defaultIntegerValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); providerMock.Setup(x => x.ResolveDoubleValue(flagName, defaultDoubleValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.InvalidContext, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); providerMock.Setup(x => x.ResolveStringValue(flagName, defaultStringValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + .ReturnsAsync(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.Setup(x => + x.ResolveStructureValue(flagName, defaultStructureValue, It.IsAny())) + .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.Setup(x => + x.ResolveStructureValue(flagName2, defaultStructureValue, It.IsAny())) + .ReturnsAsync(new ResolutionDetails(flagName2, defaultStructureValue, ErrorType.ProviderNotReady, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.Setup(x => x.ResolveStructureValue(flagName, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + providerMock.Setup(x => x.ResolveBooleanValue(flagName2, defaultBoolValue, It.IsAny())) + .ReturnsAsync(new ResolutionDetails(flagName2, defaultBoolValue, ErrorType.TargetingKeyMissing, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - providerMock.Setup(x => x.ResolveStructureValue(flagName2, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); var provider = providerMock.Object; - (await provider.ResolveBooleanValue(flagName, defaultBoolValue)).ErrorType.Should().Be(ErrorType.General); - (await provider.ResolveIntegerValue(flagName, defaultIntegerValue)).ErrorType.Should().Be(ErrorType.ParseError); - (await provider.ResolveDoubleValue(flagName, defaultDoubleValue)).ErrorType.Should().Be(ErrorType.ParseError); - (await provider.ResolveStringValue(flagName, defaultStringValue)).ErrorType.Should().Be(ErrorType.TypeMismatch); - (await provider.ResolveStructureValue(flagName, defaultStructureValue)).ErrorType.Should().Be(ErrorType.FlagNotFound); - (await provider.ResolveStructureValue(flagName2, defaultStructureValue)).ErrorType.Should().Be(ErrorType.ProviderNotReady); + var boolRes = await provider.ResolveBooleanValue(flagName, defaultBoolValue); + boolRes.ErrorType.Should().Be(ErrorType.General); + boolRes.ErrorMessage.Should().Be(testMessage); + + var intRes = await provider.ResolveIntegerValue(flagName, defaultIntegerValue); + intRes.ErrorType.Should().Be(ErrorType.ParseError); + intRes.ErrorMessage.Should().Be(testMessage); + + var doubleRes = await provider.ResolveDoubleValue(flagName, defaultDoubleValue); + doubleRes.ErrorType.Should().Be(ErrorType.InvalidContext); + doubleRes.ErrorMessage.Should().Be(testMessage); + + var stringRes = await provider.ResolveStringValue(flagName, defaultStringValue); + stringRes.ErrorType.Should().Be(ErrorType.TypeMismatch); + stringRes.ErrorMessage.Should().Be(testMessage); + + var structRes1 = await provider.ResolveStructureValue(flagName, defaultStructureValue); + structRes1.ErrorType.Should().Be(ErrorType.FlagNotFound); + structRes1.ErrorMessage.Should().Be(testMessage); + + var structRes2 = await provider.ResolveStructureValue(flagName2, defaultStructureValue); + structRes2.ErrorType.Should().Be(ErrorType.ProviderNotReady); + structRes2.ErrorMessage.Should().Be(testMessage); + + var boolRes2 = await provider.ResolveBooleanValue(flagName2, defaultBoolValue); + boolRes2.ErrorType.Should().Be(ErrorType.TargetingKeyMissing); + boolRes2.ErrorMessage.Should().BeNull(); } } } diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs index 705db1a9..45a19200 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs @@ -176,7 +176,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var client = OpenFeature.Instance.GetClient(clientName, clientVersion, mockedLogger.Object); var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); - evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch.GetDescription()); + evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); mockedFeatureProvider .Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); @@ -321,6 +321,36 @@ public async Task Should_Resolve_StructureValue() featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); } + [Fact] + public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() + { + var fixture = new Fixture(); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + const string testMessage = "Couldn't parse flag data."; + + var featureProviderMock = new Mock(MockBehavior.Strict); + featureProviderMock + .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) + .Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, + "ERROR", null, testMessage))); + featureProviderMock.Setup(x => x.GetMetadata()) + .Returns(new Metadata(fixture.Create())); + featureProviderMock.Setup(x => x.GetProviderHooks()) + .Returns(Array.Empty()); + + OpenFeature.Instance.SetProvider(featureProviderMock.Object); + var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + var response = await client.GetObjectDetails(flagName, defaultValue); + + response.ErrorType.Should().Be(ErrorType.ParseError); + response.Reason.Should().Be(Reason.Error); + response.ErrorMessage.Should().Be(testMessage); + featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); + } + [Fact] public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() { @@ -329,11 +359,12 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); + const string testMessage = "Couldn't parse flag data."; var featureProviderMock = new Mock(MockBehavior.Strict); featureProviderMock .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .Throws(new FeatureProviderException(ErrorType.ParseError)); + .Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) @@ -343,8 +374,9 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var client = OpenFeature.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); - response.ErrorType.Should().Be(ErrorType.ParseError.GetDescription()); + response.ErrorType.Should().Be(ErrorType.ParseError); response.Reason.Should().Be(Reason.Error); + response.ErrorMessage.Should().Be(testMessage); featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); } From a6e3d0cb3b18852e70f723ac227965a771c49f44 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 30 Sep 2022 08:08:24 +1000 Subject: [PATCH 042/316] chore(main): release 0.3.0 (#73) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 11 +++++++++++ build/Common.prod.props | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ccdf8aa7..6b7b74c5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.3" + ".": "0.3.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 561c15c5..6c0f35f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.3.0](https://github.com/open-feature/dotnet-sdk/compare/v0.2.3...v0.3.0) (2022-09-28) + + +### ⚠ BREAKING CHANGES + +* ErrorType as enum, add ErrorMessage string (#72) + +### Features + +* ErrorType as enum, add ErrorMessage string ([#72](https://github.com/open-feature/dotnet-sdk/issues/72)) ([e7ab498](https://github.com/open-feature/dotnet-sdk/commit/e7ab49866bd83d7b146059b0c22944a7db6956b4)) + ## [0.2.3](https://github.com/open-feature/dotnet-sdk/compare/v0.2.2...v0.2.3) (2022-09-22) diff --git a/build/Common.prod.props b/build/Common.prod.props index bc6a73c7..a0b74d09 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.2.3 + 0.3.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 71364f3b0fcf7a981e1d8000d5764aad4e3a6e68 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 30 Sep 2022 08:19:56 +1000 Subject: [PATCH 043/316] chore: add spec badge (#74) --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 41a43ed4..a6487cf8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OpenFeature SDK for .NET [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -[![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)]() +[![spec version badge](https://img.shields.io/badge/Specification-v0.5.0-yellow)](https://github.com/open-feature/spec/tree/v0.5.0?rgh-link-date=2022-09-27T17%3A53%3A52Z) [![codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) [![nuget](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://bestpractices.coreinfrastructure.org/projects/6250) @@ -12,12 +12,6 @@ OpenFeature is an open standard for feature flag management, created to support The packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) Excluding .NET Framework 3.5 -## Providers - -| Provider | Package Name | -| ----------- | ----------- | -| TBA | TBA | - ## Getting Started ### Basic Usage From b59750b6e72867781e53341da28b155ec77f8cbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Oct 2022 23:03:05 +1000 Subject: [PATCH 044/316] chore: Bump actions/setup-dotnet from 2 to 3 (#75) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dotnet-format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index d7da82b6..f1fbef59 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET Core 6.0 - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x From d980a94402bdb94cae4c60c1809f1579be7f5449 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:42:10 -0700 Subject: [PATCH 045/316] feat!: Implement builders and immutable contexts. (#77) Allow for the context provided to the client to be immutable, and for immutability for the duration of a hook. See: https://github.com/open-feature/dotnet-sdk/issues/56 Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- README.md | 8 + build/Common.props | 4 + src/OpenFeatureSDK/Hook.cs | 2 +- src/OpenFeatureSDK/Model/EvaluationContext.cs | 211 +++--------------- .../Model/EvaluationContextBuilder.cs | 145 ++++++++++++ src/OpenFeatureSDK/Model/HookContext.cs | 12 + src/OpenFeatureSDK/Model/Structure.cs | 152 +++---------- src/OpenFeatureSDK/Model/StructureBuilder.cs | 144 ++++++++++++ src/OpenFeatureSDK/Model/Value.cs | 16 +- src/OpenFeatureSDK/OpenFeature.cs | 4 +- src/OpenFeatureSDK/OpenFeatureClient.cs | 26 ++- .../OpenFeatureClientTests.cs | 43 ++-- .../OpenFeatureEvaluationContextTests.cs | 64 +++--- .../OpenFeatureHookTests.cs | 84 +++---- test/OpenFeatureSDK.Tests/OpenFeatureTests.cs | 10 +- test/OpenFeatureSDK.Tests/StructureTests.cs | 63 +++--- .../TestImplementations.cs | 2 +- test/OpenFeatureSDK.Tests/ValueTests.cs | 44 ++-- 18 files changed, 564 insertions(+), 470 deletions(-) create mode 100644 src/OpenFeatureSDK/Model/EvaluationContextBuilder.cs create mode 100644 src/OpenFeatureSDK/Model/StructureBuilder.cs diff --git a/README.md b/README.md index a6487cf8..61a87c52 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,14 @@ OpenFeature.Instance.SetProvider(new NoOpProvider()); var client = OpenFeature.Instance.GetClient(); // Evaluation the `my-feature` feature flag var isEnabled = await client.GetBooleanValue("my-feature", false); + +// Evaluating with a context. +var evaluationContext = EvaluationContext.Builder() + .Set("my-key", "my-value") + .Build(); + +// Evaluation the `my-conditional` feature flag +var isEnabled = await client.GetBooleanValue("my-conditional", false, evaluationContext); ``` ### Provider diff --git a/build/Common.props b/build/Common.props index aae4ba90..49ac8adf 100644 --- a/build/Common.props +++ b/build/Common.props @@ -21,4 +21,8 @@ [2.0,6.0) [1.0.0,2.0) + + + + diff --git a/src/OpenFeatureSDK/Hook.cs b/src/OpenFeatureSDK/Hook.cs index 5a478e20..12949f0a 100644 --- a/src/OpenFeatureSDK/Hook.cs +++ b/src/OpenFeatureSDK/Hook.cs @@ -31,7 +31,7 @@ public abstract class Hook public virtual Task Before(HookContext context, IReadOnlyDictionary hints = null) { - return Task.FromResult(new EvaluationContext()); + return Task.FromResult(EvaluationContext.Empty); } /// diff --git a/src/OpenFeatureSDK/Model/EvaluationContext.cs b/src/OpenFeatureSDK/Model/EvaluationContext.cs index c50561e6..3d10e81b 100644 --- a/src/OpenFeatureSDK/Model/EvaluationContext.cs +++ b/src/OpenFeatureSDK/Model/EvaluationContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; namespace OpenFeatureSDK.Model { @@ -8,9 +9,31 @@ namespace OpenFeatureSDK.Model /// to the feature flag evaluation context. /// /// Evaluation context - public class EvaluationContext + public sealed class EvaluationContext { - private readonly Structure _structure = new Structure(); + private readonly Structure _structure; + + /// + /// Internal constructor used by the builder. + /// + /// The content of the context. + internal EvaluationContext(Structure content) + { + this._structure = content; + } + + /// + /// Private constructor for making an empty . + /// + private EvaluationContext() + { + this._structure = Structure.Empty; + } + + /// + /// An empty evaluation context. + /// + public static EvaluationContext Empty { get; } = new EvaluationContext(); /// /// Gets the Value at the specified key @@ -35,15 +58,6 @@ public class EvaluationContext /// public bool ContainsKey(string key) => this._structure.ContainsKey(key); - /// - /// Removes the Value at the specified key - /// - /// The key of the value to be removed - /// - /// Thrown when the key is - /// - public void Remove(string key) => this._structure.Remove(key); - /// /// Gets the value associated with the specified key /// @@ -59,153 +73,9 @@ public class EvaluationContext /// Gets all values as a Dictionary /// /// New representation of this Structure - public IDictionary AsDictionary() - { - return new Dictionary(this._structure.AsDictionary()); - } - - /// - /// Add a new bool Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, bool value) + public IImmutableDictionary AsDictionary() { - this._structure.Add(key, value); - return this; - } - - /// - /// Add a new string Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, string value) - { - this._structure.Add(key, value); - return this; - } - - /// - /// Add a new int Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, int value) - { - this._structure.Add(key, value); - return this; - } - - /// - /// Add a new double Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, double value) - { - this._structure.Add(key, value); - return this; - } - - /// - /// Add a new DateTime Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, DateTime value) - { - this._structure.Add(key, value); - return this; - } - - /// - /// Add a new Structure Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, Structure value) - { - this._structure.Add(key, value); - return this; - } - - /// - /// Add a new List Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, List value) - { - this._structure.Add(key, value); - return this; - } - - /// - /// Add a new Value to the evaluation context - /// - /// The key of the value to be added - /// The value to be added - /// This - /// - /// Thrown when the key is - /// - /// - /// Thrown when an element with the same key is already contained in the context - /// - public EvaluationContext Add(string key, Value value) - { - this._structure.Add(key, value); - return this; + return this._structure.AsDictionary(); } /// @@ -214,32 +84,21 @@ public EvaluationContext Add(string key, Value value) public int Count => this._structure.Count; /// - /// Merges provided evaluation context into this one. - /// Any duplicate keys will be overwritten. + /// Return an enumerator for all values /// - /// - public void Merge(EvaluationContext other) + /// An enumerator for all values + public IEnumerator> GetEnumerator() { - foreach (var key in other._structure.Keys) - { - if (this._structure.ContainsKey(key)) - { - this._structure[key] = other._structure[key]; - } - else - { - this._structure.Add(key, other._structure[key]); - } - } + return this._structure.GetEnumerator(); } /// - /// Return an enumerator for all values + /// Get a builder which can build an . /// - /// An enumerator for all values - public IEnumerator> GetEnumerator() + /// The builder + public static EvaluationContextBuilder Builder() { - return this._structure.GetEnumerator(); + return new EvaluationContextBuilder(); } } } diff --git a/src/OpenFeatureSDK/Model/EvaluationContextBuilder.cs b/src/OpenFeatureSDK/Model/EvaluationContextBuilder.cs new file mode 100644 index 00000000..f5c88025 --- /dev/null +++ b/src/OpenFeatureSDK/Model/EvaluationContextBuilder.cs @@ -0,0 +1,145 @@ +using System; + +namespace OpenFeatureSDK.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(); + + /// + /// Internal to only allow direct creation by . + /// + internal EvaluationContextBuilder() { } + + /// + /// 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 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 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 . + /// + /// 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; + } + + /// + /// 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; + } + + /// + /// Build an immutable . + /// + /// An immutable + public EvaluationContext Build() + { + return new EvaluationContext(this._attributes.Build()); + } + } +} diff --git a/src/OpenFeatureSDK/Model/HookContext.cs b/src/OpenFeatureSDK/Model/HookContext.cs index d97b8333..329ca180 100644 --- a/src/OpenFeatureSDK/Model/HookContext.cs +++ b/src/OpenFeatureSDK/Model/HookContext.cs @@ -65,5 +65,17 @@ public HookContext(string flagKey, this.ProviderMetadata = providerMetadata ?? throw new ArgumentNullException(nameof(providerMetadata)); this.EvaluationContext = evaluationContext ?? throw new ArgumentNullException(nameof(evaluationContext)); } + + internal HookContext WithNewEvaluationContext(EvaluationContext context) + { + return new HookContext( + this.FlagKey, + this.DefaultValue, + this.FlagValueType, + this.ClientMetadata, + this.ProviderMetadata, + context + ); + } } } diff --git a/src/OpenFeatureSDK/Model/Structure.cs b/src/OpenFeatureSDK/Model/Structure.cs index eec68240..c2d0ba8d 100644 --- a/src/OpenFeatureSDK/Model/Structure.cs +++ b/src/OpenFeatureSDK/Model/Structure.cs @@ -1,6 +1,6 @@ -using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; namespace OpenFeatureSDK.Model @@ -8,25 +8,38 @@ namespace OpenFeatureSDK.Model /// /// Structure represents a map of Values /// - public class Structure : IEnumerable> + public sealed class Structure : IEnumerable> { - private readonly Dictionary _attributes; + private readonly ImmutableDictionary _attributes; /// - /// Creates a new structure with an empty set of attributes + /// Internal constructor for use by the builder. /// - public Structure() + internal Structure(ImmutableDictionary attributes) { - this._attributes = new Dictionary(); + this._attributes = attributes; } + /// + /// Private constructor for creating an empty . + /// + private Structure() + { + this._attributes = ImmutableDictionary.Empty; + } + + /// + /// An empty structure. + /// + public static Structure Empty { get; } = new Structure(); + /// /// Creates a new structure with the supplied attributes /// /// public Structure(IDictionary attributes) { - this._attributes = new Dictionary(attributes); + this._attributes = ImmutableDictionary.CreateRange(attributes); } /// @@ -43,13 +56,6 @@ public Structure(IDictionary attributes) /// indicating the presence of the key. public bool ContainsKey(string key) => this._attributes.ContainsKey(key); - /// - /// Removes the Value at the specified key - /// - /// The key of the value to be retrieved - /// indicating the presence of the key. - public bool Remove(string key) => this._attributes.Remove(key); - /// /// Gets the value associated with the specified key by mutating the supplied value. /// @@ -62,9 +68,9 @@ public Structure(IDictionary attributes) /// Gets all values as a Dictionary /// /// New representation of this Structure - public IDictionary AsDictionary() + public IImmutableDictionary AsDictionary() { - return new Dictionary(this._attributes); + return this._attributes; } /// @@ -74,114 +80,17 @@ public IDictionary AsDictionary() public Value this[string key] { get => this._attributes[key]; - set => this._attributes[key] = value; - } - - /// - /// Return a collection containing all the keys in this structure - /// - public ICollection Keys => this._attributes.Keys; - - /// - /// Return a collection containing all the values in this structure - /// - public ICollection Values => this._attributes.Values; - - /// - /// Add a new bool Value to the structure - /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, bool value) - { - this._attributes.Add(key, new Value(value)); - return this; - } - - /// - /// Add a new string Value to the structure - /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, string value) - { - this._attributes.Add(key, new Value(value)); - return this; - } - - /// - /// Add a new int Value to the structure - /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, int value) - { - this._attributes.Add(key, new Value(value)); - return this; } /// - /// Add a new double Value to the structure + /// Return a list containing all the keys in this structure /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, double value) - { - this._attributes.Add(key, new Value(value)); - return this; - } + public IImmutableList Keys => this._attributes.Keys.ToImmutableList(); /// - /// Add a new DateTime Value to the structure + /// Return an enumerable containing all the values in this structure /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, DateTime value) - { - this._attributes.Add(key, new Value(value)); - return this; - } - - /// - /// Add a new Structure Value to the structure - /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, Structure value) - { - this._attributes.Add(key, new Value(value)); - return this; - } - - /// - /// Add a new List Value to the structure - /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, IList value) - { - this._attributes.Add(key, new Value(value)); - return this; - } - - /// - /// Add a new Value to the structure - /// - /// The key of the value to be retrieved - /// The value to be added - /// This - public Structure Add(string key, Value value) - { - this._attributes.Add(key, new Value(value)); - return this; - } + public IImmutableList Values => this._attributes.Values.ToImmutableList(); /// /// Return a count of all values @@ -197,6 +106,15 @@ public IEnumerator> GetEnumerator() return this._attributes.GetEnumerator(); } + /// + /// Get a builder which can build a . + /// + /// The builder + public static StructureBuilder Builder() + { + return new StructureBuilder(); + } + [ExcludeFromCodeCoverage] IEnumerator IEnumerable.GetEnumerator() { diff --git a/src/OpenFeatureSDK/Model/StructureBuilder.cs b/src/OpenFeatureSDK/Model/StructureBuilder.cs new file mode 100644 index 00000000..5fba1422 --- /dev/null +++ b/src/OpenFeatureSDK/Model/StructureBuilder.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace OpenFeatureSDK.Model +{ + /// + /// A builder which allows the specification of attributes for a . + /// + /// A object is intended for use by a single thread and should not be used from + /// multiple threads. Once a has been created it is immutable and safe for use from + /// multiple threads. + /// + /// + public sealed class StructureBuilder + { + private readonly ImmutableDictionary.Builder _attributes = + ImmutableDictionary.CreateBuilder(); + + /// + /// Internal to only allow direct creation by . + /// + internal StructureBuilder() { } + + /// + /// Set the key to the given . + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, Value value) + { + // Remove the attribute. Will not throw an exception if not present. + this._attributes.Remove(key); + this._attributes.Add(key, value); + return this; + } + + /// + /// Set the key to the given string. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, string value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Set the key to the given int. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, int value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Set the key to the given double. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, double value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Set the key to the given long. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, long value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Set the key to the given bool. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, bool value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Set the key to the given . + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, Structure value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Set the key to the given DateTime. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, DateTime value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Set the key to the given list. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, IList value) + { + this.Set(key, new Value(value)); + return this; + } + + /// + /// Build an immutable / + /// + /// The built + public Structure Build() + { + return new Structure(this._attributes.ToImmutable()); + } + } +} diff --git a/src/OpenFeatureSDK/Model/Value.cs b/src/OpenFeatureSDK/Model/Value.cs index 308c698a..10ce3c7f 100644 --- a/src/OpenFeatureSDK/Model/Value.cs +++ b/src/OpenFeatureSDK/Model/Value.cs @@ -1,6 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; namespace OpenFeatureSDK.Model { @@ -8,7 +8,7 @@ namespace OpenFeatureSDK.Model /// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format. /// This intermediate representation provides a good medium of exchange. /// - public class Value + public sealed class Value { private readonly object _innerValue; @@ -23,6 +23,10 @@ public class Value /// The object to set as the inner value public Value(Object value) { + if (value is IList list) + { + value = list.ToImmutableList(); + } // integer is a special case, convert those. this._innerValue = value is int ? Convert.ToDouble(value) : value; if (!(this.IsNull @@ -77,8 +81,8 @@ public Value(Object value) /// /// Creates a Value with the inner set to list type /// - /// List type - public Value(IList value) => this._innerValue = value; + /// List type + public Value(IList value) => this._innerValue = value.ToImmutableList(); /// /// Creates a Value with the inner set to DateTime type @@ -120,7 +124,7 @@ public Value(Object value) /// Determines if inner value is list /// /// True if value is list - public bool IsList => this._innerValue is IList; + public bool IsList => this._innerValue is IImmutableList; /// /// Determines if inner value is DateTime @@ -174,7 +178,7 @@ public Value(Object value) /// Value will be null if it isn't a List /// /// Value as List - public IList AsList => this.IsList ? (IList)this._innerValue : null; + public IImmutableList AsList => this.IsList ? (IImmutableList)this._innerValue : null; /// /// Returns the underlying DateTime value diff --git a/src/OpenFeatureSDK/OpenFeature.cs b/src/OpenFeatureSDK/OpenFeature.cs index 8ea95048..42b23dcf 100644 --- a/src/OpenFeatureSDK/OpenFeature.cs +++ b/src/OpenFeatureSDK/OpenFeature.cs @@ -11,7 +11,7 @@ namespace OpenFeatureSDK /// public sealed class OpenFeature { - private EvaluationContext _evaluationContext = new EvaluationContext(); + private EvaluationContext _evaluationContext = EvaluationContext.Empty; private FeatureProvider _featureProvider = new NoOpFeatureProvider(); private readonly List _hooks = new List(); @@ -82,7 +82,7 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge /// Sets the global /// /// - public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? new EvaluationContext(); + public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? EvaluationContext.Empty; /// /// Gets the global diff --git a/src/OpenFeatureSDK/OpenFeatureClient.cs b/src/OpenFeatureSDK/OpenFeatureClient.cs index a105644a..3d4120b7 100644 --- a/src/OpenFeatureSDK/OpenFeatureClient.cs +++ b/src/OpenFeatureSDK/OpenFeatureClient.cs @@ -47,7 +47,7 @@ public FeatureClient(FeatureProvider featureProvider, string name, string versio this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider)); this._metadata = new ClientMetadata(name, version); this._logger = logger ?? new Logger(new NullLoggerFactory()); - this._evaluationContext = context ?? new EvaluationContext(); + this._evaluationContext = context ?? EvaluationContext.Empty; } /// @@ -213,13 +213,15 @@ private async Task> EvaluateFlag( // New up a evaluation context if one was not provided. if (context == null) { - context = new EvaluationContext(); + context = EvaluationContext.Empty; } // merge api, client, and invocation context. var evaluationContext = OpenFeature.Instance.GetContext(); - evaluationContext.Merge(this.GetContext()); - evaluationContext.Merge(context); + var evaluationContextBuilder = EvaluationContext.Builder(); + evaluationContextBuilder.Merge(evaluationContext); + evaluationContextBuilder.Merge(this.GetContext()); + evaluationContextBuilder.Merge(context); var allHooks = new List() .Concat(OpenFeature.Instance.GetHooks()) @@ -240,16 +242,16 @@ private async Task> EvaluateFlag( defaultValue, flagValueType, this._metadata, OpenFeature.Instance.GetProviderMetadata(), - evaluationContext + evaluationContextBuilder.Build() ); FlagEvaluationDetails evaluation; try { - await this.TriggerBeforeHooks(allHooks, hookContext, options); + var contextFromHooks = await this.TriggerBeforeHooks(allHooks, hookContext, options); evaluation = - (await resolveValueDelegate.Invoke(flagKey, defaultValue, hookContext.EvaluationContext)) + (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext)) .ToFlagEvaluationDetails(); await this.TriggerAfterHooks(allHooksReversed, hookContext, evaluation, options); @@ -277,15 +279,19 @@ private async Task> EvaluateFlag( return evaluation; } - private async Task TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, + private async Task> TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, FlagEvaluationOptions options) { + var evalContextBuilder = EvaluationContext.Builder(); + evalContextBuilder.Merge(context.EvaluationContext); + foreach (var hook in hooks) { var resp = await hook.Before(context, options?.HookHints); if (resp != null) { - context.EvaluationContext.Merge(resp); + evalContextBuilder.Merge(resp); + context = context.WithNewEvaluationContext(evalContextBuilder.Build()); } else { @@ -293,6 +299,8 @@ private async Task TriggerBeforeHooks(IReadOnlyList hooks, HookContext< hook.GetType().Name); } } + + return context.WithNewEvaluationContext(evalContextBuilder.Build()); } private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext context, diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs index 45a19200..6babbdf5 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs @@ -7,7 +7,6 @@ using Moq; using OpenFeatureSDK.Constant; using OpenFeatureSDK.Error; -using OpenFeatureSDK.Extension; using OpenFeatureSDK.Model; using OpenFeatureSDK.Tests.Internal; using Xunit; @@ -75,24 +74,24 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var client = OpenFeature.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); - (await client.GetBooleanValue(flagName, defaultBoolValue, new EvaluationContext())).Should().Be(defaultBoolValue); - (await client.GetBooleanValue(flagName, defaultBoolValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultBoolValue); + (await client.GetBooleanValue(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); + (await client.GetBooleanValue(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultBoolValue); (await client.GetIntegerValue(flagName, defaultIntegerValue)).Should().Be(defaultIntegerValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue, new EvaluationContext())).Should().Be(defaultIntegerValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValue(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValue(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultIntegerValue); (await client.GetDoubleValue(flagName, defaultDoubleValue)).Should().Be(defaultDoubleValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue, new EvaluationContext())).Should().Be(defaultDoubleValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValue(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValue(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultDoubleValue); (await client.GetStringValue(flagName, defaultStringValue)).Should().Be(defaultStringValue); - (await client.GetStringValue(flagName, defaultStringValue, new EvaluationContext())).Should().Be(defaultStringValue); - (await client.GetStringValue(flagName, defaultStringValue, new EvaluationContext(), emptyFlagOptions)).Should().Be(defaultStringValue); + (await client.GetStringValue(flagName, defaultStringValue, EvaluationContext.Empty)).Should().Be(defaultStringValue); + (await client.GetStringValue(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultStringValue); (await client.GetObjectValue(flagName, defaultStructureValue)).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValue(flagName, defaultStructureValue, new EvaluationContext())).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValue(flagName, defaultStructureValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValue(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValue(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(defaultStructureValue); } [Fact] @@ -122,28 +121,28 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetBooleanDetails(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetails(flagName, defaultBoolValue, new EvaluationContext())).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetails(flagName, defaultBoolValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetails(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetails(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); var integerFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetIntegerDetails(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetails(flagName, defaultIntegerValue, new EvaluationContext())).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetails(flagName, defaultIntegerValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetails(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetails(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); var doubleFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetDoubleDetails(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetails(flagName, defaultDoubleValue, new EvaluationContext())).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetails(flagName, defaultDoubleValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetails(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetails(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); var stringFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetStringDetails(flagName, defaultStringValue)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetails(flagName, defaultStringValue, new EvaluationContext())).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetails(flagName, defaultStringValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetails(flagName, defaultStringValue, EvaluationContext.Empty)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetails(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetObjectDetails(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetails(flagName, defaultStructureValue, new EvaluationContext())).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetails(flagName, defaultStructureValue, new EvaluationContext(), emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetails(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetails(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); } [Fact] @@ -393,7 +392,7 @@ public void Should_Get_And_Set_Context() var KEY = "key"; var VAL = 1; FeatureClient client = OpenFeature.Instance.GetClient(); - client.SetContext(new EvaluationContext().Add(KEY, VAL)); + client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } } diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs index 8c743568..2dc90710 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using AutoFixture; using FluentAssertions; using OpenFeatureSDK.Model; @@ -13,12 +12,12 @@ public class OpenFeatureEvaluationContextTests [Fact] public void Should_Merge_Two_Contexts() { - var context1 = new EvaluationContext() - .Add("key1", "value1"); - var context2 = new EvaluationContext() - .Add("key2", "value2"); + var contextBuilder1 = new EvaluationContextBuilder() + .Set("key1", "value1"); + var contextBuilder2 = new EvaluationContextBuilder() + .Set("key2", "value2"); - context1.Merge(context2); + var context1 = contextBuilder1.Merge(contextBuilder2.Build()).Build(); Assert.Equal(2, context1.Count); Assert.Equal("value1", context1.GetValue("key1").AsString); @@ -29,21 +28,18 @@ public void Should_Merge_Two_Contexts() [Specification("3.2.2", "Duplicate values being overwritten.")] public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Context() { - var context1 = new EvaluationContext(); - var context2 = new EvaluationContext(); + var contextBuilder1 = new EvaluationContextBuilder(); + var contextBuilder2 = new EvaluationContextBuilder(); - context1.Add("key1", "value1"); - context2.Add("key1", "overriden_value"); - context2.Add("key2", "value2"); + contextBuilder1.Set("key1", "value1"); + contextBuilder2.Set("key1", "overriden_value"); + contextBuilder2.Set("key2", "value2"); - context1.Merge(context2); + var context1 = contextBuilder1.Merge(contextBuilder2.Build()).Build(); Assert.Equal(2, context1.Count); Assert.Equal("overriden_value", context1.GetValue("key1").AsString); Assert.Equal("value2", context1.GetValue("key2").AsString); - - context1.Remove("key1"); - Assert.Throws(() => context1.GetValue("key1")); } [Fact] @@ -54,13 +50,15 @@ public void EvaluationContext_Should_All_Types() var fixture = new Fixture(); var now = fixture.Create(); var structure = fixture.Create(); - var context = new EvaluationContext() - .Add("key1", "value") - .Add("key2", 1) - .Add("key3", true) - .Add("key4", now) - .Add("key5", structure) - .Add("key6", 1.0); + var contextBuilder = new EvaluationContextBuilder() + .Set("key1", "value") + .Set("key2", 1) + .Set("key3", true) + .Set("key4", now) + .Set("key5", structure) + .Set("key6", 1.0); + + var context = contextBuilder.Build(); var value1 = context.GetValue("key1"); value1.IsString.Should().BeTrue(); @@ -89,24 +87,24 @@ public void EvaluationContext_Should_All_Types() [Fact] [Specification("3.1.4", "The evaluation context fields MUST have an unique key.")] - public void When_Duplicate_Key_Throw_Unique_Constraint() + public void When_Duplicate_Key_Set_It_Replaces_Value() { - var context = new EvaluationContext().Add("key", "value"); - var exception = Assert.Throws(() => - context.Add("key", "overriden_value")); - exception.Message.Should().StartWith("An item with the same key has already been added."); + var contextBuilder = new EvaluationContextBuilder().Set("key", "value"); + contextBuilder.Set("key", "overriden_value"); + Assert.Equal("overriden_value", contextBuilder.Build().GetValue("key").AsString); } [Fact] [Specification("3.1.3", "The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")] public void Should_Be_Able_To_Get_All_Values() { - var context = new EvaluationContext() - .Add("key1", "value1") - .Add("key2", "value2") - .Add("key3", "value3") - .Add("key4", "value4") - .Add("key5", "value5"); + var context = new EvaluationContextBuilder() + .Set("key1", "value1") + .Set("key2", "value2") + .Set("key3", "value3") + .Set("key4", "value4") + .Set("key5", "value5") + .Build(); // Iterate over key value pairs and check consistency var count = 0; diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs index 8266e2d7..0c97e18b 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs @@ -33,19 +33,19 @@ public async Task Hooks_Should_Be_Called_In_Order() apiHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); clientHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); invocationHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); providerHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); providerHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), It.IsAny>(), @@ -82,7 +82,7 @@ public async Task Hooks_Should_Be_Called_In_Order() var client = OpenFeature.Instance.GetClient(clientName, clientVersion); client.AddHooks(clientHook.Object); - await client.GetBooleanValue(flagName, defaultValue, new EvaluationContext(), + await client.GetBooleanValue(flagName, defaultValue, EvaluationContext.Empty, new FlagEvaluationOptions(invocationHook.Object, new Dictionary())); apiHook.Verify(x => x.Before( @@ -127,19 +127,19 @@ public async Task Hooks_Should_Be_Called_In_Order() public void Hook_Context_Should_Not_Allow_Nulls() { Assert.Throws(() => - new HookContext(null, new Structure(), FlagValueType.Object, new ClientMetadata(null, null), - new Metadata(null), new EvaluationContext())); + new HookContext(null, Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null), + new Metadata(null), EvaluationContext.Empty)); Assert.Throws(() => - new HookContext("test", new Structure(), FlagValueType.Object, null, - new Metadata(null), new EvaluationContext())); + new HookContext("test", Structure.Empty, FlagValueType.Object, null, + new Metadata(null), EvaluationContext.Empty)); Assert.Throws(() => - new HookContext("test", new Structure(), FlagValueType.Object, new ClientMetadata(null, null), - null, new EvaluationContext())); + new HookContext("test", Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null), + null, EvaluationContext.Empty)); Assert.Throws(() => - new HookContext("test", new Structure(), FlagValueType.Object, new ClientMetadata(null, null), + new HookContext("test", Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null), new Metadata(null), null)); } @@ -150,9 +150,9 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() { var clientMetadata = new ClientMetadata("client", "1.0.0"); var providerMetadata = new Metadata("provider"); - var testStructure = new Structure(); + var testStructure = Structure.Empty; var context = new HookContext("test", testStructure, FlagValueType.Object, clientMetadata, - providerMetadata, new EvaluationContext()); + providerMetadata, EvaluationContext.Empty); context.ClientMetadata.Should().BeSameAs(clientMetadata); context.ProviderMetadata.Should().BeSameAs(providerMetadata); @@ -166,7 +166,7 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() [Specification("4.3.3", "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")] public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() { - var evaluationContext = new EvaluationContext().Add("test", "test"); + var evaluationContext = new EvaluationContextBuilder().Set("test", "test").Build(); var hook1 = new Mock(MockBehavior.Strict); var hook2 = new Mock(MockBehavior.Strict); var hookContext = new HookContext("test", false, @@ -181,7 +181,7 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() .ReturnsAsync(evaluationContext); var client = OpenFeature.Instance.GetClient("test", "1.0.0"); - await client.GetBooleanValue("test", false, new EvaluationContext(), + await client.GetBooleanValue("test", false, EvaluationContext.Empty, new FlagEvaluationOptions(new[] { hook1.Object, hook2.Object }, new Dictionary())); hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); @@ -204,23 +204,27 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() var propHook = "4.3.4hook"; // setup a cascade of overwriting properties - OpenFeature.Instance.SetContext(new EvaluationContext() - .Add(propGlobal, true) - .Add(propGlobalToOverwrite, false)); - - var clientContext = new EvaluationContext() - .Add(propClient, true) - .Add(propGlobalToOverwrite, true) - .Add(propClientToOverwrite, false); - - var invocationContext = new EvaluationContext() - .Add(propInvocation, true) - .Add(propClientToOverwrite, true) - .Add(propInvocationToOverwrite, false); - - var hookContext = new EvaluationContext() - .Add(propHook, true) - .Add(propInvocationToOverwrite, true); + OpenFeature.Instance.SetContext(new EvaluationContextBuilder() + .Set(propGlobal, true) + .Set(propGlobalToOverwrite, false) + .Build()); + + var clientContext = new EvaluationContextBuilder() + .Set(propClient, true) + .Set(propGlobalToOverwrite, true) + .Set(propClientToOverwrite, false) + .Build(); + + var invocationContext = new EvaluationContextBuilder() + .Set(propInvocation, true) + .Set(propClientToOverwrite, true) + .Set(propInvocationToOverwrite, false) + .Build(); + + var hookContext = new EvaluationContextBuilder() + .Set(propHook, true) + .Set(propInvocationToOverwrite, true) + .Build(); var provider = new Mock(MockBehavior.Strict); @@ -270,10 +274,10 @@ public async Task Hook_Should_Return_No_Errors() ["number"] = 1, ["boolean"] = true, ["datetime"] = DateTime.Now, - ["structure"] = new Structure() + ["structure"] = Structure.Empty }; var hookContext = new HookContext("test", false, FlagValueType.Boolean, - new ClientMetadata(null, null), new Metadata(null), new EvaluationContext()); + new ClientMetadata(null, null), new Metadata(null), EvaluationContext.Empty); await hook.Before(hookContext, hookHints); await hook.After(hookContext, new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"), hookHints); @@ -307,7 +311,7 @@ public async Task Hook_Should_Execute_In_Correct_Order() hook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); featureProvider.InSequence(sequence) .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) @@ -372,11 +376,11 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() hook1.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), null)) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); hook2.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), null)) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); featureProvider.InSequence(sequence) .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) @@ -431,11 +435,11 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() hook1.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), null)) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); hook2.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), null)) - .ReturnsAsync(new EvaluationContext()); + .ReturnsAsync(EvaluationContext.Empty); featureProvider1.InSequence(sequence) .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs index 5150f032..5f9a778b 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs @@ -1,4 +1,3 @@ -using AutoFixture; using FluentAssertions; using Moq; using OpenFeatureSDK.Constant; @@ -79,8 +78,13 @@ public void OpenFeature_Should_Create_Client(string name = null, string version [Fact] public void Should_Set_Given_Context() { - var fixture = new Fixture(); - var context = fixture.Create(); + var context = EvaluationContext.Empty; + + OpenFeature.Instance.SetContext(context); + + OpenFeature.Instance.GetContext().Should().BeSameAs(context); + + context = EvaluationContext.Builder().Build(); OpenFeature.Instance.SetContext(context); diff --git a/test/OpenFeatureSDK.Tests/StructureTests.cs b/test/OpenFeatureSDK.Tests/StructureTests.cs index 2e78a62f..12bde7fb 100644 --- a/test/OpenFeatureSDK.Tests/StructureTests.cs +++ b/test/OpenFeatureSDK.Tests/StructureTests.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using FluentAssertions; using OpenFeatureSDK.Model; using Xunit; @@ -10,18 +13,16 @@ public class StructureTests [Fact] public void No_Arg_Should_Contain_Empty_Attributes() { - Structure structure = new Structure(); + Structure structure = Structure.Empty; Assert.Equal(0, structure.Count); - Assert.Equal(0, structure.AsDictionary().Keys.Count); + Assert.Empty(structure.AsDictionary()); } [Fact] public void Dictionary_Arg_Should_Contain_New_Dictionary() { string KEY = "key"; - IDictionary dictionary = new Dictionary(){ - { KEY, new Value(KEY) } - }; + IDictionary dictionary = new Dictionary() { { KEY, new Value(KEY) } }; Structure structure = new Structure(dictionary); Assert.Equal(KEY, structure.AsDictionary()[KEY].AsString); Assert.NotSame(structure.AsDictionary(), dictionary); // should be a copy @@ -44,19 +45,20 @@ public void Add_And_Get_Add_And_Return_Values() int INT_VAL = 13; double DOUBLE_VAL = .5; DateTime DATE_VAL = DateTime.Now; - Structure STRUCT_VAL = new Structure(); + Structure STRUCT_VAL = Structure.Empty; IList LIST_VAL = new List(); Value VALUE_VAL = new Value(); - Structure structure = new Structure(); - structure.Add(BOOL_KEY, BOOL_VAL); - structure.Add(STRING_KEY, STRING_VAL); - structure.Add(INT_KEY, INT_VAL); - structure.Add(DOUBLE_KEY, DOUBLE_VAL); - structure.Add(DATE_KEY, DATE_VAL); - structure.Add(STRUCT_KEY, STRUCT_VAL); - structure.Add(LIST_KEY, LIST_VAL); - structure.Add(VALUE_KEY, VALUE_VAL); + var structureBuilder = Structure.Builder(); + structureBuilder.Set(BOOL_KEY, BOOL_VAL); + structureBuilder.Set(STRING_KEY, STRING_VAL); + structureBuilder.Set(INT_KEY, INT_VAL); + structureBuilder.Set(DOUBLE_KEY, DOUBLE_VAL); + structureBuilder.Set(DATE_KEY, DATE_VAL); + structureBuilder.Set(STRUCT_KEY, STRUCT_VAL); + structureBuilder.Set(LIST_KEY, ImmutableList.CreateRange(LIST_VAL)); + structureBuilder.Set(VALUE_KEY, VALUE_VAL); + var structure = structureBuilder.Build(); Assert.Equal(BOOL_VAL, structure.GetValue(BOOL_KEY).AsBoolean); Assert.Equal(STRING_VAL, structure.GetValue(STRING_KEY).AsString); @@ -68,27 +70,14 @@ public void Add_And_Get_Add_And_Return_Values() Assert.True(structure.GetValue(VALUE_KEY).IsNull); } - [Fact] - public void Remove_Should_Remove_Value() - { - String KEY = "key"; - bool VAL = true; - - Structure structure = new Structure(); - structure.Add(KEY, VAL); - Assert.Equal(1, structure.Count); - structure.Remove(KEY); - Assert.Equal(0, structure.Count); - } - [Fact] public void TryGetValue_Should_Return_Value() { String KEY = "key"; String VAL = "val"; - Structure structure = new Structure(); - structure.Add(KEY, VAL); + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); Value value; Assert.True(structure.TryGetValue(KEY, out value)); Assert.Equal(VAL, value.AsString); @@ -100,8 +89,8 @@ public void Values_Should_Return_Values() String KEY = "key"; Value VAL = new Value("val"); - Structure structure = new Structure(); - structure.Add(KEY, VAL); + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); Assert.Equal(1, structure.Values.Count); } @@ -111,10 +100,10 @@ public void Keys_Should_Return_Keys() String KEY = "key"; Value VAL = new Value("val"); - Structure structure = new Structure(); - structure.Add(KEY, VAL); + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); Assert.Equal(1, structure.Keys.Count); - Assert.True(structure.Keys.Contains(KEY)); + Assert.Equal(0, structure.Keys.IndexOf(KEY)); } [Fact] @@ -123,8 +112,8 @@ public void GetEnumerator_Should_Return_Enumerator() string KEY = "key"; string VAL = "val"; - Structure structure = new Structure(); - structure.Add(KEY, VAL); + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); IEnumerator> enumerator = structure.GetEnumerator(); enumerator.MoveNext(); Assert.Equal(VAL, enumerator.Current.Value.AsString); diff --git a/test/OpenFeatureSDK.Tests/TestImplementations.cs b/test/OpenFeatureSDK.Tests/TestImplementations.cs index 980aea01..4236077c 100644 --- a/test/OpenFeatureSDK.Tests/TestImplementations.cs +++ b/test/OpenFeatureSDK.Tests/TestImplementations.cs @@ -11,7 +11,7 @@ public class TestHook : Hook { public override Task Before(HookContext context, IReadOnlyDictionary hints = null) { - return Task.FromResult(new EvaluationContext()); + return Task.FromResult(EvaluationContext.Empty); } public override Task After(HookContext context, FlagEvaluationDetails details, diff --git a/test/OpenFeatureSDK.Tests/ValueTests.cs b/test/OpenFeatureSDK.Tests/ValueTests.cs index cf3f214c..8c3ae37d 100644 --- a/test/OpenFeatureSDK.Tests/ValueTests.cs +++ b/test/OpenFeatureSDK.Tests/ValueTests.cs @@ -7,7 +7,9 @@ namespace OpenFeatureSDK.Tests { public class ValueTests { - class Foo { } + class Foo + { + } [Fact] public void No_Arg_Should_Contain_Null() @@ -19,24 +21,23 @@ public void No_Arg_Should_Contain_Null() [Fact] public void Object_Arg_Should_Contain_Object() { - try + // int is a special case, see Int_Object_Arg_Should_Contain_Object() + IList list = new List() { - // int is a special case, see Int_Object_Arg_Should_Contain_Object() - IList list = new List(){ - true, "val", .5, new Structure(), new List(), DateTime.Now - }; + true, + "val", + .5, + Structure.Empty, + new List(), + DateTime.Now + }; - int i = 0; - foreach (Object l in list) - { - Value value = new Value(l); - Assert.Equal(list[i], value.AsObject); - i++; - } - } - catch (Exception) + int i = 0; + foreach (Object l in list) { - Assert.True(false, "Expected no exception."); + Value value = new Value(l); + Assert.Equal(list[i], value.AsObject); + i++; } } @@ -80,7 +81,7 @@ public void Numeric_Arg_Should_Return_Double_Or_Int() double innerDoubleValue = .75; Value doubleValue = new Value(innerDoubleValue); Assert.True(doubleValue.IsNumber); - Assert.Equal(1, doubleValue.AsInteger); // should be rounded + Assert.Equal(1, doubleValue.AsInteger); // should be rounded Assert.Equal(.75, doubleValue.AsDouble); int innerIntValue = 100; @@ -113,20 +114,17 @@ public void Structure_Arg_Should_Contain_Structure() { string INNER_KEY = "key"; string INNER_VALUE = "val"; - Structure innerValue = new Structure().Add(INNER_KEY, INNER_VALUE); + Structure innerValue = Structure.Builder().Set(INNER_KEY, INNER_VALUE).Build(); Value value = new Value(innerValue); Assert.True(value.IsStructure); Assert.Equal(INNER_VALUE, value.AsStructure.GetValue(INNER_KEY).AsString); } [Fact] - public void LIst_Arg_Should_Contain_LIst() + public void List_Arg_Should_Contain_List() { string ITEM_VALUE = "val"; - IList innerValue = new List() - { - new Value(ITEM_VALUE) - }; + IList innerValue = new List() { new Value(ITEM_VALUE) }; Value value = new Value(innerValue); Assert.True(value.IsList); Assert.Equal(ITEM_VALUE, value.AsList[0].AsString); From 6b70c30013ea9a25472888bb1b45da18e1ba19a8 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 11 Oct 2022 18:31:20 -0400 Subject: [PATCH 046/316] chore: exclude component in git tag (#80) --- release-please-config.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-please-config.json b/release-please-config.json index 061b9cd7..515dc1e4 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -2,6 +2,8 @@ "packages": { ".": { "release-type": "simple", + "monorepo-tags": false, + "include-component-in-tag": false, "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "versioning": "default", From 609016fc86f8eee8d848a9227b57aaef0d9b85b0 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 12 Oct 2022 11:27:06 -0700 Subject: [PATCH 047/316] feat!: Thread safe hooks, provider, and context (#79) --- src/OpenFeatureSDK/FeatureProvider.cs | 7 +- .../Model/FlagEvaluationOptions.cs | 20 +-- src/OpenFeatureSDK/OpenFeature.cs | 114 +++++++++++++++--- src/OpenFeatureSDK/OpenFeatureClient.cs | 109 +++++++++++++---- .../OpenFeatureClientTests.cs | 35 +++--- .../OpenFeatureHookTests.cs | 38 +++--- test/OpenFeatureSDK.Tests/OpenFeatureTests.cs | 9 +- .../TestImplementations.cs | 3 +- 8 files changed, 245 insertions(+), 90 deletions(-) diff --git a/src/OpenFeatureSDK/FeatureProvider.cs b/src/OpenFeatureSDK/FeatureProvider.cs index 639363a8..e2934cea 100644 --- a/src/OpenFeatureSDK/FeatureProvider.cs +++ b/src/OpenFeatureSDK/FeatureProvider.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; using OpenFeatureSDK.Model; @@ -22,8 +21,8 @@ public abstract class FeatureProvider /// error (if applicable): Provider, Invocation, Client, API /// finally: Provider, Invocation, Client, API /// - /// - public virtual IReadOnlyList GetProviderHooks() => Array.Empty(); + /// Immutable list of hooks + public virtual IImmutableList GetProviderHooks() => ImmutableList.Empty; /// /// Metadata describing the provider. diff --git a/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs b/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs index cb8bc353..38396723 100644 --- a/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Immutable; namespace OpenFeatureSDK.Model { @@ -12,33 +12,33 @@ public class FlagEvaluationOptions /// /// A immutable list of /// - public IReadOnlyList Hooks { get; } + public IImmutableList Hooks { get; } /// /// A immutable dictionary of hook hints /// - public IReadOnlyDictionary HookHints { get; } + 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(IReadOnlyList hooks, IReadOnlyDictionary hookHints = null) + public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary hookHints = null) { this.Hooks = hooks; - this.HookHints = hookHints ?? new Dictionary(); + 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, IReadOnlyDictionary hookHints = null) + public FlagEvaluationOptions(Hook hook, ImmutableDictionary hookHints = null) { - this.Hooks = new[] { hook }; - this.HookHints = hookHints ?? new Dictionary(); + this.Hooks = ImmutableList.Create(hook); + this.HookHints = hookHints ?? ImmutableDictionary.Empty; } } } diff --git a/src/OpenFeatureSDK/OpenFeature.cs b/src/OpenFeatureSDK/OpenFeature.cs index 42b23dcf..cf119f9f 100644 --- a/src/OpenFeatureSDK/OpenFeature.cs +++ b/src/OpenFeatureSDK/OpenFeature.cs @@ -1,4 +1,8 @@ +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Threading; using Microsoft.Extensions.Logging; using OpenFeatureSDK.Model; @@ -13,7 +17,11 @@ public sealed class OpenFeature { private EvaluationContext _evaluationContext = EvaluationContext.Empty; private FeatureProvider _featureProvider = new NoOpFeatureProvider(); - private readonly List _hooks = new List(); + private readonly ConcurrentStack _hooks = new ConcurrentStack(); + + /// The reader/writer locks are not disposed because the singleton instance should never be disposed. + private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim(); + private readonly ReaderWriterLockSlim _featureProviderLock = new ReaderWriterLockSlim(); /// /// Singleton instance of OpenFeature @@ -30,19 +38,53 @@ private OpenFeature() { } /// Sets the feature provider /// /// Implementation of - public void SetProvider(FeatureProvider featureProvider) => this._featureProvider = featureProvider; + public void SetProvider(FeatureProvider featureProvider) + { + this._featureProviderLock.EnterWriteLock(); + try + { + this._featureProvider = featureProvider; + } + finally + { + this._featureProviderLock.ExitWriteLock(); + } + } /// /// 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() => this._featureProvider; + public FeatureProvider GetProvider() + { + this._featureProviderLock.EnterReadLock(); + try + { + return this._featureProvider; + } + finally + { + this._featureProviderLock.ExitReadLock(); + } + } /// /// 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._featureProvider.GetMetadata(); + public Metadata GetProviderMetadata() => this.GetProvider().GetMetadata(); /// /// Create a new instance of using the current provider @@ -52,26 +94,39 @@ private OpenFeature() { } /// 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._featureProvider, name, version, logger, context); + public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null, + EvaluationContext context = null) => + new FeatureClient(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) => this._hooks.AddRange(hooks); + public void AddHooks(IEnumerable hooks) => this._hooks.PushRange(hooks.ToArray()); /// /// Adds a hook to global hooks list + /// + /// Hooks which are dependent on each other should be provided in a collection + /// using the . + /// /// - /// A list of - public void AddHooks(Hook hook) => this._hooks.Add(hook); + /// Hook that implements the interface + public void AddHooks(Hook hook) => this._hooks.Push(hook); /// - /// Returns the global immutable hooks list + /// 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. + /// /// - /// A immutable list of - public IReadOnlyList GetHooks() => this._hooks.AsReadOnly(); + /// Enumeration of + public IEnumerable GetHooks() => this._hooks.Reverse(); /// /// Removes all hooks from global hooks list @@ -81,13 +136,40 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge /// /// Sets the global /// - /// - public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? EvaluationContext.Empty; + /// The to set + public void SetContext(EvaluationContext context) + { + this._evaluationContextLock.EnterWriteLock(); + try + { + this._evaluationContext = context ?? EvaluationContext.Empty; + } + finally + { + this._evaluationContextLock.ExitWriteLock(); + } + } /// /// 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. + /// /// - /// - public EvaluationContext GetContext() => this._evaluationContext; + /// An + public EvaluationContext GetContext() + { + this._evaluationContextLock.EnterReadLock(); + try + { + return this._evaluationContext; + } + finally + { + this._evaluationContextLock.ExitReadLock(); + } + } } } diff --git a/src/OpenFeatureSDK/OpenFeatureClient.cs b/src/OpenFeatureSDK/OpenFeatureClient.cs index 3d4120b7..efe47cb6 100644 --- a/src/OpenFeatureSDK/OpenFeatureClient.cs +++ b/src/OpenFeatureSDK/OpenFeatureClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -17,34 +18,80 @@ namespace OpenFeatureSDK public sealed class FeatureClient : IFeatureClient { private readonly ClientMetadata _metadata; - private readonly FeatureProvider _featureProvider; - private readonly List _hooks = new List(); + private readonly ConcurrentStack _hooks = new ConcurrentStack(); private readonly ILogger _logger; private EvaluationContext _evaluationContext; + private readonly object _evaluationContextLock = new object(); + + /// + /// Get a provider and an associated typed flag resolution method. + /// + /// The global provider could change between two accesses, so in order to safely get provider information we + /// must first alias it and then use that alias to access everything we need. + /// + /// + /// + /// This method should return the desired flag resolution method from the given provider reference. + /// + /// The type of the resolution method + /// A tuple containing a resolution method and the provider it came from. + private (Func>>, FeatureProvider) + ExtractProvider( + Func>>> method) + { + // Alias the provider reference so getting the method and returning the provider are + // guaranteed to be the same object. + var provider = OpenFeature.Instance.GetProvider(); + + if (provider == null) + { + provider = new NoOpFeatureProvider(); + this._logger.LogDebug("No provider configured, using no-op provider"); + } + + return (method(provider), provider); + } + /// /// Gets the EvaluationContext 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 - public EvaluationContext GetContext() => this._evaluationContext; + public EvaluationContext GetContext() + { + lock (this._evaluationContextLock) + { + return this._evaluationContext; + } + } /// /// Sets the EvaluationContext of the client /// - public void SetContext(EvaluationContext evaluationContext) => this._evaluationContext = evaluationContext; + /// The to set + public void SetContext(EvaluationContext context) + { + lock (this._evaluationContextLock) + { + this._evaluationContext = context ?? EvaluationContext.Empty; + } + } /// /// Initializes a new instance of the class. /// - /// Feature provider used by client /// Name of client /// Version of client /// Logger used by client /// Context given to this client /// Throws if any of the required parameters are null - public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null, EvaluationContext context = null) + public FeatureClient(string name, string version, ILogger logger = null, EvaluationContext context = null) { - this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider)); this._metadata = new ClientMetadata(name, version); this._logger = logger ?? new Logger(new NullLoggerFactory()); this._evaluationContext = context ?? EvaluationContext.Empty; @@ -58,21 +105,33 @@ public FeatureClient(FeatureProvider featureProvider, string name, string versio /// /// Add hook to client + /// + /// 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.Add(hook); + public void AddHooks(Hook hook) => this._hooks.Push(hook); /// /// Appends hooks to client + /// + /// The appending operation will be atomic. + /// /// /// A list of Hooks that implement the interface - public void AddHooks(IEnumerable hooks) => this._hooks.AddRange(hooks); + public void AddHooks(IEnumerable hooks) => this._hooks.PushRange(hooks.ToArray()); /// - /// Return a immutable list of hooks that are registered against the client + /// 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. + /// /// - /// A list of immutable hooks - public IReadOnlyList GetHooks() => this._hooks.ToList().AsReadOnly(); + /// Enumeration of + public IEnumerable GetHooks() => this._hooks.Reverse(); /// /// Removes all hooks from the client @@ -101,7 +160,8 @@ public async Task GetBooleanValue(string flagKey, bool defaultValue, Evalu /// Resolved flag details public async Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - await this.EvaluateFlag(this._featureProvider.ResolveBooleanValue, FlagValueType.Boolean, flagKey, + await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue), + FlagValueType.Boolean, flagKey, defaultValue, context, config); /// @@ -126,7 +186,8 @@ public async Task GetStringValue(string flagKey, string defaultValue, Ev /// Resolved flag details public async Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - await this.EvaluateFlag(this._featureProvider.ResolveStringValue, FlagValueType.String, flagKey, + await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue), + FlagValueType.String, flagKey, defaultValue, context, config); /// @@ -151,7 +212,8 @@ public async Task GetIntegerValue(string flagKey, int defaultValue, Evaluat /// Resolved flag details public async Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - await this.EvaluateFlag(this._featureProvider.ResolveIntegerValue, FlagValueType.Number, flagKey, + await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue), + FlagValueType.Number, flagKey, defaultValue, context, config); /// @@ -177,7 +239,8 @@ public async Task GetDoubleValue(string flagKey, double defaultValue, /// Resolved flag details public async Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - await this.EvaluateFlag(this._featureProvider.ResolveDoubleValue, FlagValueType.Number, flagKey, + await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue), + FlagValueType.Number, flagKey, defaultValue, context, config); /// @@ -202,14 +265,18 @@ public async Task GetObjectValue(string flagKey, Value defaultValue, Eval /// Resolved flag details public async Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - await this.EvaluateFlag(this._featureProvider.ResolveStructureValue, FlagValueType.Object, flagKey, + await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue), + FlagValueType.Object, flagKey, defaultValue, context, config); private async Task> EvaluateFlag( - Func>> resolveValueDelegate, + (Func>>, FeatureProvider) providerInfo, FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions options = null) { + var resolveValueDelegate = providerInfo.Item1; + var provider = providerInfo.Item2; + // New up a evaluation context if one was not provided. if (context == null) { @@ -225,9 +292,9 @@ private async Task> EvaluateFlag( var allHooks = new List() .Concat(OpenFeature.Instance.GetHooks()) - .Concat(this._hooks) + .Concat(this.GetHooks()) .Concat(options?.Hooks ?? Enumerable.Empty()) - .Concat(this._featureProvider.GetProviderHooks()) + .Concat(provider.GetProviderHooks()) .ToList() .AsReadOnly(); @@ -241,7 +308,7 @@ private async Task> EvaluateFlag( flagKey, defaultValue, flagValueType, this._metadata, - OpenFeature.Instance.GetProviderMetadata(), + provider.GetMetadata(), evaluationContextBuilder.Build() ); diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs index 6babbdf5..519fe452 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; @@ -30,14 +32,14 @@ public void OpenFeatureClient_Should_Allow_Hooks() client.AddHooks(new[] { hook1, hook2 }); client.GetHooks().Should().ContainInOrder(hook1, hook2); - client.GetHooks().Count.Should().Be(2); + client.GetHooks().Count().Should().Be(2); client.AddHooks(hook3); client.GetHooks().Should().ContainInOrder(hook1, hook2, hook3); - client.GetHooks().Count.Should().Be(3); + client.GetHooks().Count().Should().Be(3); client.ClearHooks(); - client.GetHooks().Count.Should().Be(0); + Assert.Empty(client.GetHooks()); } [Fact] @@ -68,7 +70,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); - var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); + var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -114,7 +116,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); - var emptyFlagOptions = new FlagEvaluationOptions(new List(), new Dictionary()); + var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -169,7 +171,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedFeatureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); mockedFeatureProvider.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(mockedFeatureProvider.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion, mockedLogger.Object); @@ -206,7 +208,7 @@ public async Task Should_Resolve_BooleanValue() featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -232,7 +234,7 @@ public async Task Should_Resolve_StringValue() featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -258,7 +260,7 @@ public async Task Should_Resolve_IntegerValue() featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -284,7 +286,7 @@ public async Task Should_Resolve_DoubleValue() featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -310,7 +312,7 @@ public async Task Should_Resolve_StructureValue() featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -338,7 +340,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -367,7 +369,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() featureProviderMock.Setup(x => x.GetMetadata()) .Returns(new Metadata(fixture.Create())); featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); OpenFeature.Instance.SetProvider(featureProviderMock.Object); var client = OpenFeature.Instance.GetClient(clientName, clientVersion); @@ -380,10 +382,11 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() } [Fact] - public void Should_Throw_ArgumentNullException_When_Provider_Is_Null() + public async Task Should_Use_No_Op_When_Provider_Is_Null() { - TestProvider provider = null; - Assert.Throws(() => new FeatureClient(provider, "test", "test")); + OpenFeature.Instance.SetProvider(null); + var client = new FeatureClient("test", "test"); + (await client.GetIntegerValue("some-key", 12)).Should().Be(12); } [Fact] diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs index 0c97e18b..3b13e6b8 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; @@ -83,7 +85,7 @@ public async Task Hooks_Should_Be_Called_In_Order() client.AddHooks(clientHook.Object); await client.GetBooleanValue(flagName, defaultValue, EvaluationContext.Empty, - new FlagEvaluationOptions(invocationHook.Object, new Dictionary())); + new FlagEvaluationOptions(invocationHook.Object, ImmutableDictionary.Empty)); apiHook.Verify(x => x.Before( It.IsAny>(), It.IsAny>()), Times.Once); @@ -173,19 +175,19 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName), evaluationContext); - hook1.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) + hook1.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) .ReturnsAsync(evaluationContext); hook2.Setup(x => - x.Before(hookContext, It.IsAny>())) + x.Before(hookContext, It.IsAny>())) .ReturnsAsync(evaluationContext); var client = OpenFeature.Instance.GetClient("test", "1.0.0"); await client.GetBooleanValue("test", false, EvaluationContext.Empty, - new FlagEvaluationOptions(new[] { hook1.Object, hook2.Object }, new Dictionary())); + new FlagEvaluationOptions(ImmutableList.Create(hook1.Object, hook2.Object), ImmutableDictionary.Empty)); - hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), It.IsAny>()), Times.Once); + hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); + hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), It.IsAny>()), Times.Once); } [Fact] @@ -232,7 +234,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() .Returns(new Metadata(null)); provider.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); provider.Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails("test", true)); @@ -240,12 +242,12 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() OpenFeature.Instance.SetProvider(provider.Object); var hook = new Mock(MockBehavior.Strict); - hook.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) + hook.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) .ReturnsAsync(hookContext); var client = OpenFeature.Instance.GetClient("test", "1.0.0", null, clientContext); - await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(new[] { hook.Object }, new Dictionary())); + await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook.Object), ImmutableDictionary.Empty)); // after proper merging, all properties should equal true provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => @@ -307,7 +309,7 @@ public async Task Hook_Should_Execute_In_Correct_Order() .Returns(new Metadata(null)); featureProvider.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); hook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) @@ -351,10 +353,10 @@ public async Task Register_Hooks_Should_Be_Available_At_All_Levels() var client = OpenFeature.Instance.GetClient(); client.AddHooks(hook2.Object); await client.GetBooleanValue("test", false, null, - new FlagEvaluationOptions(hook3.Object, new Dictionary())); + new FlagEvaluationOptions(hook3.Object, ImmutableDictionary.Empty)); - OpenFeature.Instance.GetHooks().Count.Should().Be(1); - client.GetHooks().Count.Should().Be(1); + Assert.Single(OpenFeature.Instance.GetHooks()); + client.GetHooks().Count().Should().Be(1); testProvider.GetProviderHooks().Count.Should().Be(1); } @@ -372,7 +374,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() .Returns(new Metadata(null)); featureProvider.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); hook1.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), null)) @@ -405,7 +407,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() OpenFeature.Instance.SetProvider(featureProvider.Object); var client = OpenFeature.Instance.GetClient(); client.AddHooks(new[] { hook1.Object, hook2.Object }); - client.GetHooks().Count.Should().Be(2); + client.GetHooks().Count().Should().Be(2); await client.GetBooleanValue("test", false); @@ -431,7 +433,7 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() featureProvider1.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); featureProvider1.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); hook1.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), null)) @@ -478,7 +480,7 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ featureProvider.Setup(x => x.GetMetadata()) .Returns(new Metadata(null)); featureProvider.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); hook1.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), It.IsAny>())) @@ -518,7 +520,7 @@ public async Task Hook_Hints_May_Be_Optional() .Returns(new Metadata(null)); featureProvider.Setup(x => x.GetProviderHooks()) - .Returns(Array.Empty()); + .Returns(ImmutableList.Empty); hook.InSequence(sequence) .Setup(x => x.Before(It.IsAny>(), defaultEmptyHookHints)) diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs index 5f9a778b..8a96e84b 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs +++ b/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using FluentAssertions; using Moq; using OpenFeatureSDK.Constant; @@ -34,18 +35,18 @@ public void OpenFeature_Should_Add_Hooks() openFeature.AddHooks(hook1); openFeature.GetHooks().Should().Contain(hook1); - openFeature.GetHooks().Count.Should().Be(1); + Assert.Single(openFeature.GetHooks()); openFeature.AddHooks(hook2); openFeature.GetHooks().Should().ContainInOrder(hook1, hook2); - openFeature.GetHooks().Count.Should().Be(2); + openFeature.GetHooks().Count().Should().Be(2); openFeature.AddHooks(new[] { hook3, hook4 }); openFeature.GetHooks().Should().ContainInOrder(hook1, hook2, hook3, hook4); - openFeature.GetHooks().Count.Should().Be(4); + openFeature.GetHooks().Count().Should().Be(4); openFeature.ClearHooks(); - openFeature.GetHooks().Count.Should().Be(0); + Assert.Empty(openFeature.GetHooks()); } [Fact] diff --git a/test/OpenFeatureSDK.Tests/TestImplementations.cs b/test/OpenFeatureSDK.Tests/TestImplementations.cs index 4236077c..2da88c7b 100644 --- a/test/OpenFeatureSDK.Tests/TestImplementations.cs +++ b/test/OpenFeatureSDK.Tests/TestImplementations.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; using OpenFeatureSDK.Model; @@ -39,7 +40,7 @@ public class TestProvider : FeatureProvider public void AddHook(Hook hook) => this._hooks.Add(hook); - public override IReadOnlyList GetProviderHooks() => this._hooks.AsReadOnly(); + public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList(); public override Metadata GetMetadata() { From d554057fa3b9e07caa3cc51d7be24f7fb34718d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 05:57:51 +1000 Subject: [PATCH 048/316] chore: Bump amannn/action-semantic-pull-request from 4 to 5 (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 4 to 5.
Release notes

Sourced from amannn/action-semantic-pull-request's releases.

v5.0.0

5.0.0 (2022-10-11)

⚠ BREAKING CHANGES

  • Enum options need to be newline delimited (to allow whitespace within them) (#205)

Features

  • Enum options need to be newline delimited (to allow whitespace within them) (#205) (c906fe1)

v4.6.0

4.6.0 (2022-09-26)

Features

  • Provide error messages as outputs.error_message (#194) (880a3c0)

v4.5.0

4.5.0 (2022-05-04)

Features

v4.4.0

4.4.0 (2022-04-22)

Features

  • Add options to pass custom regex to conventional-commits-parser (#177) (956659a)

v4.3.0

4.3.0 (2022-04-13)

Features

  • Add ignoreLabels option to opt-out of validation for certain PRs (#174) (277c230)

v4.2.0

... (truncated)

Changelog

Sourced from amannn/action-semantic-pull-request's changelog.

4.5.0 (2022-05-04)

Features

4.4.0 (2022-04-22)

Features

  • Add options to pass custom regex to conventional-commits-parser (#177) (956659a)

4.3.0 (2022-04-13)

Features

  • Add ignoreLabels option to opt-out of validation for certain PRs (#174) (277c230)

4.2.0 (2022-02-08)

Features

  • Add opt-in validation that PR titles match a single commit (#160) (c05e358)

4.1.0 (2022-02-04)

Features

  • Check if the PR title matches the commit title when single commits are validated to avoid surprises (#158) (f1216e9)

4.0.1 (2022-02-03)

Bug Fixes

4.0.0 (2022-02-02)

⚠ BREAKING CHANGES

  • dropped support for node <=15

Features

... (truncated)

Commits
  • 5369185 chore: Release 5.0.0 [skip ci]
  • c906fe1 feat!: Enum options need to be newline delimited (to allow whitespace within ...
  • b314c1b docs: Improve example for composing outputs (#206)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=amannn/action-semantic-pull-request&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index cd91d143..279e8f55 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -12,6 +12,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v4 + - uses: amannn/action-semantic-pull-request@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 8b738da959c887b7b718cf307853376b2e4f933f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 06:05:52 +1000 Subject: [PATCH 049/316] chore(main): release 0.4.0 (#78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [0.4.0](https://github.com/open-feature/dotnet-sdk/compare/v0.3.0...v0.4.0) (2022-10-12) ### ⚠ BREAKING CHANGES * Thread safe hooks, provider, and context (#79) * Implement builders and immutable contexts. (#77) ### Features * Implement builders and immutable contexts. ([#77](https://github.com/open-feature/dotnet-sdk/issues/77)) ([d980a94](https://github.com/open-feature/dotnet-sdk/commit/d980a94402bdb94cae4c60c1809f1579be7f5449)) * Thread safe hooks, provider, and context ([#79](https://github.com/open-feature/dotnet-sdk/issues/79)) ([609016f](https://github.com/open-feature/dotnet-sdk/commit/609016fc86f8eee8d848a9227b57aaef0d9b85b0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 13 +++++++++++++ build/Common.prod.props | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6b7b74c5..da59f99e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.0" + ".": "0.4.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0f35f4..069eac81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.4.0](https://github.com/open-feature/dotnet-sdk/compare/v0.3.0...v0.4.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* Thread safe hooks, provider, and context (#79) +* Implement builders and immutable contexts. (#77) + +### Features + +* Implement builders and immutable contexts. ([#77](https://github.com/open-feature/dotnet-sdk/issues/77)) ([d980a94](https://github.com/open-feature/dotnet-sdk/commit/d980a94402bdb94cae4c60c1809f1579be7f5449)) +* Thread safe hooks, provider, and context ([#79](https://github.com/open-feature/dotnet-sdk/issues/79)) ([609016f](https://github.com/open-feature/dotnet-sdk/commit/609016fc86f8eee8d848a9227b57aaef0d9b85b0)) + ## [0.3.0](https://github.com/open-feature/dotnet-sdk/compare/v0.2.3...v0.3.0) (2022-09-28) diff --git a/build/Common.prod.props b/build/Common.prod.props index a0b74d09..c721ae7e 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.3.0 + 0.4.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 6090bd971817cc6cc8b74487b2850d8e99a2c94d Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sun, 16 Oct 2022 11:08:37 +1000 Subject: [PATCH 050/316] feat!: rename OpenFeature class to API and ns to OpenFeature (#82) To make it easier to reuse the OpenFeature namespace, we need to ensure no classes/structs are named OpenFeature. --- .github/workflows/release.yml | 4 +- OpenFeatureSDK.proj => OpenFeature.proj | 0 OpenFeatureSDK.sln => OpenFeature.sln | 4 +- README.md | 12 ++-- build/Common.tests.props | 2 +- src/Directory.Build.props | 2 +- .../OpenFeature.cs => OpenFeature/Api.cs} | 21 +++---- .../Constant/ErrorType.cs | 2 +- .../Constant/FlagValueType.cs | 2 +- .../Constant/NoOpProvider.cs | 2 +- .../Constant/Reason.cs | 2 +- .../Error/FeatureProviderException.cs | 4 +- .../Extension/EnumExtensions.cs | 2 +- .../Extension/ResolutionDetailsExtensions.cs | 4 +- .../FeatureProvider.cs | 4 +- src/{OpenFeatureSDK => OpenFeature}/Hook.cs | 4 +- .../IFeatureClient.cs | 4 +- .../Model/ClientMetadata.cs | 2 +- .../Model/EvaluationContext.cs | 2 +- .../Model/EvaluationContextBuilder.cs | 2 +- .../Model/FlagEvaluationDetails.cs | 4 +- .../Model/FlagEvaluationOptions.cs | 2 +- .../Model/HookContext.cs | 4 +- .../Model/Metadata.cs | 8 +-- .../Model/ResolutionDetails.cs | 4 +- .../Model/Structure.cs | 2 +- .../Model/StructureBuilder.cs | 2 +- .../Model/Value.cs | 2 +- .../NoOpProvider.cs | 6 +- .../OpenFeature.csproj} | 4 +- .../OpenFeatureClient.cs | 20 +++--- test/Directory.Build.props | 2 +- .../ClearOpenFeatureInstanceFixture.cs | 13 ++++ .../FeatureProviderExceptionTests.cs | 8 +-- .../FeatureProviderTests.cs | 8 +-- .../Internal/SpecificationAttribute.cs | 2 +- .../OpenFeature.Tests.csproj} | 4 +- .../OpenFeatureClientTests.cs | 61 +++++++++---------- .../OpenFeatureEvaluationContextTests.cs | 6 +- .../OpenFeatureHookTests.cs | 52 ++++++++-------- .../OpenFeatureTests.cs | 30 ++++----- .../StructureTests.cs | 6 +- .../TestImplementations.cs | 4 +- .../ValueTests.cs | 4 +- .../ClearOpenFeatureInstanceFixture.cs | 13 ---- 45 files changed, 174 insertions(+), 178 deletions(-) rename OpenFeatureSDK.proj => OpenFeature.proj (100%) rename OpenFeatureSDK.sln => OpenFeature.sln (93%) rename src/{OpenFeatureSDK/OpenFeature.cs => OpenFeature/Api.cs} (91%) rename src/{OpenFeatureSDK => OpenFeature}/Constant/ErrorType.cs (94%) rename src/{OpenFeatureSDK => OpenFeature}/Constant/FlagValueType.cs (89%) rename src/{OpenFeatureSDK => OpenFeature}/Constant/NoOpProvider.cs (83%) rename src/{OpenFeatureSDK => OpenFeature}/Constant/Reason.cs (95%) rename src/{OpenFeatureSDK => OpenFeature}/Error/FeatureProviderException.cs (91%) rename src/{OpenFeatureSDK => OpenFeature}/Extension/EnumExtensions.cs (90%) rename src/{OpenFeatureSDK => OpenFeature}/Extension/ResolutionDetailsExtensions.cs (82%) rename src/{OpenFeatureSDK => OpenFeature}/FeatureProvider.cs (96%) rename src/{OpenFeatureSDK => OpenFeature}/Hook.cs (96%) rename src/{OpenFeatureSDK => OpenFeature}/IFeatureClient.cs (95%) rename src/{OpenFeatureSDK => OpenFeature}/Model/ClientMetadata.cs (92%) rename src/{OpenFeatureSDK => OpenFeature}/Model/EvaluationContext.cs (96%) rename src/{OpenFeatureSDK => OpenFeature}/Model/EvaluationContextBuilder.cs (99%) rename src/{OpenFeatureSDK => OpenFeature}/Model/FlagEvaluationDetails.cs (94%) rename src/{OpenFeatureSDK => OpenFeature}/Model/FlagEvaluationOptions.cs (96%) rename src/{OpenFeatureSDK => OpenFeature}/Model/HookContext.cs (95%) rename src/{OpenFeatureSDK => OpenFeature}/Model/Metadata.cs (60%) rename src/{OpenFeatureSDK => OpenFeature}/Model/ResolutionDetails.cs (94%) rename src/{OpenFeatureSDK => OpenFeature}/Model/Structure.cs (96%) rename src/{OpenFeatureSDK => OpenFeature}/Model/StructureBuilder.cs (99%) rename src/{OpenFeatureSDK => OpenFeature}/Model/Value.cs (97%) rename src/{OpenFeatureSDK => OpenFeature}/NoOpProvider.cs (93%) rename src/{OpenFeatureSDK/OpenFeatureSDK.csproj => OpenFeature/OpenFeature.csproj} (80%) rename src/{OpenFeatureSDK => OpenFeature}/OpenFeatureClient.cs (95%) create mode 100644 test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/FeatureProviderExceptionTests.cs (89%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/FeatureProviderTests.cs (97%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/Internal/SpecificationAttribute.cs (87%) rename test/{OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj => OpenFeature.Tests/OpenFeature.Tests.csproj} (91%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/OpenFeatureClientTests.cs (90%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/OpenFeatureEvaluationContextTests.cs (95%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/OpenFeatureHookTests.cs (93%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/OpenFeatureTests.cs (77%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/StructureTests.cs (95%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/TestImplementations.cs (95%) rename test/{OpenFeatureSDK.Tests => OpenFeature.Tests}/ValueTests.cs (95%) delete mode 100644 test/OpenFeatureSDK.Tests/ClearOpenFeatureInstanceFixture.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9dc750fd..048160b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,11 +34,11 @@ jobs: - name: Pack if: ${{ steps.release.outputs.releases_created }} run: | - dotnet pack OpenFeatureSDK.proj --configuration Release --no-build -p:PackageID=OpenFeature + dotnet pack OpenFeature.proj --configuration Release --no-build -p:PackageID=OpenFeature - name: Publish to Nuget if: ${{ steps.release.outputs.releases_created }} run: | - dotnet nuget push src/OpenFeatureSDK/bin/Release/OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg ` + dotnet nuget push src/OpenFeature/bin/Release/OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg ` --api-key ${{secrets.NUGET_TOKEN}} ` --source https://api.nuget.org/v3/index.json diff --git a/OpenFeatureSDK.proj b/OpenFeature.proj similarity index 100% rename from OpenFeatureSDK.proj rename to OpenFeature.proj diff --git a/OpenFeatureSDK.sln b/OpenFeature.sln similarity index 93% rename from OpenFeatureSDK.sln rename to OpenFeature.sln index 064f140e..9b75048f 100644 --- a/OpenFeatureSDK.sln +++ b/OpenFeature.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeatureSDK", "src\OpenFeatureSDK\OpenFeatureSDK.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}" ProjectSection(SolutionItems) = preProject @@ -29,7 +29,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{65FBA159-2 test\Directory.Build.props = test\Directory.Build.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeatureSDK.Tests", "test\OpenFeatureSDK.Tests\OpenFeatureSDK.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Tests", "test\OpenFeature.Tests\OpenFeature.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index 61a87c52..24bd9811 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ The packages will aim to support all current .NET versions. Refer to the current ### Basic Usage ```csharp -using OpenFeatureSDK; +using OpenFeature; // Sets the provider used by the client -OpenFeature.Instance.SetProvider(new NoOpProvider()); +Api.Instance.SetProvider(new NoOpProvider()); // Gets a instance of the feature flag client var client = OpenFeature.Instance.GetClient(); // Evaluation the `my-feature` feature flag @@ -42,8 +42,8 @@ To develop a provider, you need to create a new project and include the OpenFeat Example of implementing a feature flag provider ```csharp -using OpenFeatureSDK; -using OpenFeatureSDK.Model; +using OpenFeature; +using OpenFeature.Model; public class MyFeatureProvider : FeatureProvider { @@ -94,10 +94,10 @@ Example of adding a hook ```csharp // add a hook globally, to run on all evaluations -openFeature.AddHooks(new ExampleGlobalHook()); +Api.Instance.AddHooks(new ExampleGlobalHook()); // add a hook on this client, to run on all evaluations made by this client -var client = OpenFeature.Instance.GetClient(); +var client = Api.Instance.GetClient(); client.AddHooks(new ExampleClientHook()); // add a hook for this evaluation only diff --git a/build/Common.tests.props b/build/Common.tests.props index 677a51ec..d4a6454e 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -10,7 +10,7 @@ - + PreserveNewest diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 47c2b439..e9839283 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,3 +1,3 @@ - + diff --git a/src/OpenFeatureSDK/OpenFeature.cs b/src/OpenFeature/Api.cs similarity index 91% rename from src/OpenFeatureSDK/OpenFeature.cs rename to src/OpenFeature/Api.cs index cf119f9f..b01d7b94 100644 --- a/src/OpenFeatureSDK/OpenFeature.cs +++ b/src/OpenFeature/Api.cs @@ -1,19 +1,18 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; -using OpenFeatureSDK.Model; +using OpenFeature.Model; -namespace OpenFeatureSDK +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 OpenFeature + public sealed class Api { private EvaluationContext _evaluationContext = EvaluationContext.Empty; private FeatureProvider _featureProvider = new NoOpFeatureProvider(); @@ -24,15 +23,15 @@ public sealed class OpenFeature private readonly ReaderWriterLockSlim _featureProviderLock = new ReaderWriterLockSlim(); /// - /// Singleton instance of OpenFeature + /// Singleton instance of Api /// - public static OpenFeature Instance { get; } = new OpenFeature(); + public static Api Instance { get; } = 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 OpenFeature() { } - private OpenFeature() { } + static Api() { } + private Api() { } /// /// Sets the feature provider @@ -58,7 +57,7 @@ public void SetProvider(FeatureProvider featureProvider) /// 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 - /// . + /// . /// /// /// @@ -80,7 +79,7 @@ public FeatureProvider GetProvider() /// /// 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 . + /// For multiple dependent provider operations see . /// ///
/// @@ -111,7 +110,7 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge /// Adds a hook to global hooks list /// /// Hooks which are dependent on each other should be provided in a collection - /// using the . + /// using the . /// ///
/// Hook that implements the interface diff --git a/src/OpenFeatureSDK/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs similarity index 94% rename from src/OpenFeatureSDK/Constant/ErrorType.cs rename to src/OpenFeature/Constant/ErrorType.cs index f1b8464d..496eed72 100644 --- a/src/OpenFeatureSDK/Constant/ErrorType.cs +++ b/src/OpenFeature/Constant/ErrorType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace OpenFeatureSDK.Constant +namespace OpenFeature.Constant { /// /// These errors are used to indicate abnormal execution when evaluation a flag diff --git a/src/OpenFeatureSDK/Constant/FlagValueType.cs b/src/OpenFeature/Constant/FlagValueType.cs similarity index 89% rename from src/OpenFeatureSDK/Constant/FlagValueType.cs rename to src/OpenFeature/Constant/FlagValueType.cs index 5eb328cd..94a35d5b 100644 --- a/src/OpenFeatureSDK/Constant/FlagValueType.cs +++ b/src/OpenFeature/Constant/FlagValueType.cs @@ -1,4 +1,4 @@ -namespace OpenFeatureSDK.Constant +namespace OpenFeature.Constant { /// /// Used to identity what object type of flag being evaluated diff --git a/src/OpenFeatureSDK/Constant/NoOpProvider.cs b/src/OpenFeature/Constant/NoOpProvider.cs similarity index 83% rename from src/OpenFeatureSDK/Constant/NoOpProvider.cs rename to src/OpenFeature/Constant/NoOpProvider.cs index e3f8eee5..0c58ec4d 100644 --- a/src/OpenFeatureSDK/Constant/NoOpProvider.cs +++ b/src/OpenFeature/Constant/NoOpProvider.cs @@ -1,4 +1,4 @@ -namespace OpenFeatureSDK.Constant +namespace OpenFeature.Constant { internal static class NoOpProvider { diff --git a/src/OpenFeatureSDK/Constant/Reason.cs b/src/OpenFeature/Constant/Reason.cs similarity index 95% rename from src/OpenFeatureSDK/Constant/Reason.cs rename to src/OpenFeature/Constant/Reason.cs index 81729809..8e233d5e 100644 --- a/src/OpenFeatureSDK/Constant/Reason.cs +++ b/src/OpenFeature/Constant/Reason.cs @@ -1,4 +1,4 @@ -namespace OpenFeatureSDK.Constant +namespace OpenFeature.Constant { /// /// Common reasons used during flag resolution diff --git a/src/OpenFeatureSDK/Error/FeatureProviderException.cs b/src/OpenFeature/Error/FeatureProviderException.cs similarity index 91% rename from src/OpenFeatureSDK/Error/FeatureProviderException.cs rename to src/OpenFeature/Error/FeatureProviderException.cs index 6a4955c3..d5c7743a 100644 --- a/src/OpenFeatureSDK/Error/FeatureProviderException.cs +++ b/src/OpenFeature/Error/FeatureProviderException.cs @@ -1,7 +1,7 @@ using System; -using OpenFeatureSDK.Constant; +using OpenFeature.Constant; -namespace OpenFeatureSDK.Error +namespace OpenFeature.Error { /// /// Used to represent an abnormal error when evaluating a flag. This exception should be thrown diff --git a/src/OpenFeatureSDK/Extension/EnumExtensions.cs b/src/OpenFeature/Extension/EnumExtensions.cs similarity index 90% rename from src/OpenFeatureSDK/Extension/EnumExtensions.cs rename to src/OpenFeature/Extension/EnumExtensions.cs index 2094486c..155bfd3e 100644 --- a/src/OpenFeatureSDK/Extension/EnumExtensions.cs +++ b/src/OpenFeature/Extension/EnumExtensions.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Linq; -namespace OpenFeatureSDK.Extension +namespace OpenFeature.Extension { internal static class EnumExtensions { diff --git a/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs similarity index 82% rename from src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs rename to src/OpenFeature/Extension/ResolutionDetailsExtensions.cs index 1580e062..616e530a 100644 --- a/src/OpenFeatureSDK/Extension/ResolutionDetailsExtensions.cs +++ b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs @@ -1,6 +1,6 @@ -using OpenFeatureSDK.Model; +using OpenFeature.Model; -namespace OpenFeatureSDK.Extension +namespace OpenFeature.Extension { internal static class ResolutionDetailsExtensions { diff --git a/src/OpenFeatureSDK/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs similarity index 96% rename from src/OpenFeatureSDK/FeatureProvider.cs rename to src/OpenFeature/FeatureProvider.cs index e2934cea..3209e810 100644 --- a/src/OpenFeatureSDK/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,8 +1,8 @@ using System.Collections.Immutable; using System.Threading.Tasks; -using OpenFeatureSDK.Model; +using OpenFeature.Model; -namespace OpenFeatureSDK +namespace OpenFeature { /// /// The provider interface describes the abstraction layer for a feature flag provider. diff --git a/src/OpenFeatureSDK/Hook.cs b/src/OpenFeature/Hook.cs similarity index 96% rename from src/OpenFeatureSDK/Hook.cs rename to src/OpenFeature/Hook.cs index 12949f0a..a4fc4065 100644 --- a/src/OpenFeatureSDK/Hook.cs +++ b/src/OpenFeature/Hook.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using OpenFeatureSDK.Model; +using OpenFeature.Model; -namespace OpenFeatureSDK +namespace OpenFeature { /// /// The Hook abstract class describes the default implementation for a hook. diff --git a/src/OpenFeatureSDK/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs similarity index 95% rename from src/OpenFeatureSDK/IFeatureClient.cs rename to src/OpenFeature/IFeatureClient.cs index 9c1fdfcc..f1fd4ad8 100644 --- a/src/OpenFeatureSDK/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Threading.Tasks; -using OpenFeatureSDK.Model; +using OpenFeature.Model; -namespace OpenFeatureSDK +namespace OpenFeature { internal interface IFeatureClient { diff --git a/src/OpenFeatureSDK/Model/ClientMetadata.cs b/src/OpenFeature/Model/ClientMetadata.cs similarity index 92% rename from src/OpenFeatureSDK/Model/ClientMetadata.cs rename to src/OpenFeature/Model/ClientMetadata.cs index bfe80895..6f148026 100644 --- a/src/OpenFeatureSDK/Model/ClientMetadata.cs +++ b/src/OpenFeature/Model/ClientMetadata.cs @@ -1,4 +1,4 @@ -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// Represents the client metadata diff --git a/src/OpenFeatureSDK/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs similarity index 96% rename from src/OpenFeatureSDK/Model/EvaluationContext.cs rename to src/OpenFeature/Model/EvaluationContext.cs index 3d10e81b..ccdcbac7 100644 --- a/src/OpenFeatureSDK/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// A KeyValuePair with a string key and object value that is used to apply user defined properties diff --git a/src/OpenFeatureSDK/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs similarity index 99% rename from src/OpenFeatureSDK/Model/EvaluationContextBuilder.cs rename to src/OpenFeature/Model/EvaluationContextBuilder.cs index f5c88025..57afa5cf 100644 --- a/src/OpenFeatureSDK/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -1,6 +1,6 @@ using System; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// A builder which allows the specification of attributes for an . diff --git a/src/OpenFeatureSDK/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs similarity index 94% rename from src/OpenFeatureSDK/Model/FlagEvaluationDetails.cs rename to src/OpenFeature/Model/FlagEvaluationDetails.cs index a70b90a5..d2c92c80 100644 --- a/src/OpenFeatureSDK/Model/FlagEvaluationDetails.cs +++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs @@ -1,6 +1,6 @@ -using OpenFeatureSDK.Constant; +using OpenFeature.Constant; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// The contract returned to the caller that describes the result of the flag evaluation process. diff --git a/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs similarity index 96% rename from src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs rename to src/OpenFeature/Model/FlagEvaluationOptions.cs index 38396723..b3a3b196 100644 --- a/src/OpenFeatureSDK/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// A structure containing the one or more hooks and hook hints diff --git a/src/OpenFeatureSDK/Model/HookContext.cs b/src/OpenFeature/Model/HookContext.cs similarity index 95% rename from src/OpenFeatureSDK/Model/HookContext.cs rename to src/OpenFeature/Model/HookContext.cs index 329ca180..f67b3b0c 100644 --- a/src/OpenFeatureSDK/Model/HookContext.cs +++ b/src/OpenFeature/Model/HookContext.cs @@ -1,7 +1,7 @@ using System; -using OpenFeatureSDK.Constant; +using OpenFeature.Constant; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// Context provided to hook execution diff --git a/src/OpenFeatureSDK/Model/Metadata.cs b/src/OpenFeature/Model/Metadata.cs similarity index 60% rename from src/OpenFeatureSDK/Model/Metadata.cs rename to src/OpenFeature/Model/Metadata.cs index 9228b462..0f8629e3 100644 --- a/src/OpenFeatureSDK/Model/Metadata.cs +++ b/src/OpenFeature/Model/Metadata.cs @@ -1,19 +1,19 @@ -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// - /// metadata + /// metadata /// public class Metadata { /// - /// Gets name of instance + /// Gets name of instance /// public string Name { get; } /// /// Initializes a new instance of the class. /// - /// Name of instance + /// Name of instance public Metadata(string name) { this.Name = name; diff --git a/src/OpenFeatureSDK/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs similarity index 94% rename from src/OpenFeatureSDK/Model/ResolutionDetails.cs rename to src/OpenFeature/Model/ResolutionDetails.cs index b9f3d36a..1e8f882d 100644 --- a/src/OpenFeatureSDK/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -1,6 +1,6 @@ -using OpenFeatureSDK.Constant; +using OpenFeature.Constant; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// Defines the contract that the is required to return diff --git a/src/OpenFeatureSDK/Model/Structure.cs b/src/OpenFeature/Model/Structure.cs similarity index 96% rename from src/OpenFeatureSDK/Model/Structure.cs rename to src/OpenFeature/Model/Structure.cs index c2d0ba8d..8bf4b4c8 100644 --- a/src/OpenFeatureSDK/Model/Structure.cs +++ b/src/OpenFeature/Model/Structure.cs @@ -3,7 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// Structure represents a map of Values diff --git a/src/OpenFeatureSDK/Model/StructureBuilder.cs b/src/OpenFeature/Model/StructureBuilder.cs similarity index 99% rename from src/OpenFeatureSDK/Model/StructureBuilder.cs rename to src/OpenFeature/Model/StructureBuilder.cs index 5fba1422..4c44813d 100644 --- a/src/OpenFeatureSDK/Model/StructureBuilder.cs +++ b/src/OpenFeature/Model/StructureBuilder.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// A builder which allows the specification of attributes for a . diff --git a/src/OpenFeatureSDK/Model/Value.cs b/src/OpenFeature/Model/Value.cs similarity index 97% rename from src/OpenFeatureSDK/Model/Value.cs rename to src/OpenFeature/Model/Value.cs index 10ce3c7f..c7f60c44 100644 --- a/src/OpenFeatureSDK/Model/Value.cs +++ b/src/OpenFeature/Model/Value.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; -namespace OpenFeatureSDK.Model +namespace OpenFeature.Model { /// /// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format. diff --git a/src/OpenFeatureSDK/NoOpProvider.cs b/src/OpenFeature/NoOpProvider.cs similarity index 93% rename from src/OpenFeatureSDK/NoOpProvider.cs rename to src/OpenFeature/NoOpProvider.cs index 94fa7c2e..6b29da2e 100644 --- a/src/OpenFeatureSDK/NoOpProvider.cs +++ b/src/OpenFeature/NoOpProvider.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Model; +using OpenFeature.Constant; +using OpenFeature.Model; -namespace OpenFeatureSDK +namespace OpenFeature { internal class NoOpFeatureProvider : FeatureProvider { diff --git a/src/OpenFeatureSDK/OpenFeatureSDK.csproj b/src/OpenFeature/OpenFeature.csproj similarity index 80% rename from src/OpenFeatureSDK/OpenFeatureSDK.csproj rename to src/OpenFeature/OpenFeature.csproj index 3e564e99..77a03baf 100644 --- a/src/OpenFeatureSDK/OpenFeatureSDK.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -2,7 +2,7 @@ netstandard2.0;net462 - OpenFeatureSDK + OpenFeature @@ -11,7 +11,7 @@ - <_Parameter1>OpenFeatureSDK.Tests + <_Parameter1>OpenFeature.Tests diff --git a/src/OpenFeatureSDK/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs similarity index 95% rename from src/OpenFeatureSDK/OpenFeatureClient.cs rename to src/OpenFeature/OpenFeatureClient.cs index efe47cb6..f33fb1a7 100644 --- a/src/OpenFeatureSDK/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -5,12 +5,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Error; -using OpenFeatureSDK.Extension; -using OpenFeatureSDK.Model; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Extension; +using OpenFeature.Model; -namespace OpenFeatureSDK +namespace OpenFeature { /// /// @@ -42,7 +42,7 @@ public sealed class FeatureClient : IFeatureClient { // Alias the provider reference so getting the method and returning the provider are // guaranteed to be the same object. - var provider = OpenFeature.Instance.GetProvider(); + var provider = Api.Instance.GetProvider(); if (provider == null) { @@ -93,7 +93,7 @@ public void SetContext(EvaluationContext context) public FeatureClient(string name, string version, ILogger logger = null, EvaluationContext context = null) { this._metadata = new ClientMetadata(name, version); - this._logger = logger ?? new Logger(new NullLoggerFactory()); + this._logger = logger ?? new Logger(new NullLoggerFactory()); this._evaluationContext = context ?? EvaluationContext.Empty; } @@ -107,7 +107,7 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// Add hook to client /// /// Hooks which are dependent on each other should be provided in a collection - /// using the . + /// using the . /// /// /// Hook that implements the interface @@ -284,14 +284,14 @@ private async Task> EvaluateFlag( } // merge api, client, and invocation context. - var evaluationContext = OpenFeature.Instance.GetContext(); + var evaluationContext = Api.Instance.GetContext(); var evaluationContextBuilder = EvaluationContext.Builder(); evaluationContextBuilder.Merge(evaluationContext); evaluationContextBuilder.Merge(this.GetContext()); evaluationContextBuilder.Merge(context); var allHooks = new List() - .Concat(OpenFeature.Instance.GetHooks()) + .Concat(Api.Instance.GetHooks()) .Concat(this.GetHooks()) .Concat(options?.Hooks ?? Enumerable.Empty()) .Concat(provider.GetProviderHooks()) diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 5e1104b8..1487b265 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,3 +1,3 @@ - + diff --git a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs new file mode 100644 index 00000000..3a0ab349 --- /dev/null +++ b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs @@ -0,0 +1,13 @@ +namespace OpenFeature.Tests +{ + public class ClearOpenFeatureInstanceFixture + { + // Make sure the singleton is cleared between tests + public ClearOpenFeatureInstanceFixture() + { + Api.Instance.SetContext(null); + Api.Instance.ClearHooks(); + Api.Instance.SetProvider(new NoOpFeatureProvider()); + } + } +} diff --git a/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs similarity index 89% rename from test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs rename to test/OpenFeature.Tests/FeatureProviderExceptionTests.cs index 5506af38..6a2f895c 100644 --- a/test/OpenFeatureSDK.Tests/FeatureProviderExceptionTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs @@ -1,11 +1,11 @@ using System; using FluentAssertions; -using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Error; -using OpenFeatureSDK.Extension; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Extension; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class FeatureProviderExceptionTests { diff --git a/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs similarity index 97% rename from test/OpenFeatureSDK.Tests/FeatureProviderTests.cs rename to test/OpenFeature.Tests/FeatureProviderTests.cs index 37a62877..c050f31d 100644 --- a/test/OpenFeatureSDK.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -2,12 +2,12 @@ using AutoFixture; using FluentAssertions; using Moq; -using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Model; -using OpenFeatureSDK.Tests.Internal; +using OpenFeature.Constant; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { diff --git a/test/OpenFeatureSDK.Tests/Internal/SpecificationAttribute.cs b/test/OpenFeature.Tests/Internal/SpecificationAttribute.cs similarity index 87% rename from test/OpenFeatureSDK.Tests/Internal/SpecificationAttribute.cs rename to test/OpenFeature.Tests/Internal/SpecificationAttribute.cs index 68a79ab3..7c0aac2a 100644 --- a/test/OpenFeatureSDK.Tests/Internal/SpecificationAttribute.cs +++ b/test/OpenFeature.Tests/Internal/SpecificationAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace OpenFeatureSDK.Tests.Internal +namespace OpenFeature.Tests.Internal { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] public class SpecificationAttribute : Attribute diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj similarity index 91% rename from test/OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj rename to test/OpenFeature.Tests/OpenFeature.Tests.csproj index ec5b0216..c620e976 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureSDK.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -3,7 +3,7 @@ net6.0 $(TargetFrameworks);net462 - OpenFeatureSDK.Tests + OpenFeature.Tests @@ -27,7 +27,7 @@ - + diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs similarity index 90% rename from test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs rename to test/OpenFeature.Tests/OpenFeatureClientTests.cs index 519fe452..87ec10e5 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -7,13 +6,13 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Error; -using OpenFeatureSDK.Model; -using OpenFeatureSDK.Tests.Internal; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture { @@ -27,7 +26,7 @@ public void OpenFeatureClient_Should_Allow_Hooks() var hook2 = new Mock(MockBehavior.Strict).Object; var hook3 = new Mock(MockBehavior.Strict).Object; - var client = OpenFeature.Instance.GetClient(clientName); + var client = Api.Instance.GetClient(clientName); client.AddHooks(new[] { hook1, hook2 }); @@ -49,7 +48,7 @@ public void OpenFeatureClient_Metadata_Should_Have_Name() var fixture = new Fixture(); var clientName = fixture.Create(); var clientVersion = fixture.Create(); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(clientName, clientVersion); client.GetMetadata().Name.Should().Be(clientName); client.GetMetadata().Version.Should().Be(clientVersion); @@ -72,8 +71,8 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(new NoOpFeatureProvider()); + var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); (await client.GetBooleanValue(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); @@ -118,8 +117,8 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(new NoOpFeatureProvider()); + var client = Api.Instance.GetClient(clientName, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetBooleanDetails(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); @@ -162,7 +161,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var flagName = fixture.Create(); var defaultValue = fixture.Create(); var mockedFeatureProvider = new Mock(MockBehavior.Strict); - var mockedLogger = new Mock>(MockBehavior.Default); + var mockedLogger = new Mock>(MockBehavior.Default); // This will fail to case a String to TestStructure mockedFeatureProvider @@ -173,8 +172,8 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedFeatureProvider.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(mockedFeatureProvider.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion, mockedLogger.Object); + Api.Instance.SetProvider(mockedFeatureProvider.Object); + var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger.Object); var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); @@ -210,8 +209,8 @@ public async Task Should_Resolve_BooleanValue() featureProviderMock.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(featureProviderMock.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(featureProviderMock.Object); + var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -236,8 +235,8 @@ public async Task Should_Resolve_StringValue() featureProviderMock.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(featureProviderMock.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(featureProviderMock.Object); + var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetStringValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -262,8 +261,8 @@ public async Task Should_Resolve_IntegerValue() featureProviderMock.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(featureProviderMock.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(featureProviderMock.Object); + var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -288,8 +287,8 @@ public async Task Should_Resolve_DoubleValue() featureProviderMock.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(featureProviderMock.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(featureProviderMock.Object); + var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -314,8 +313,8 @@ public async Task Should_Resolve_StructureValue() featureProviderMock.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(featureProviderMock.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(featureProviderMock.Object); + var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -342,8 +341,8 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() featureProviderMock.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(featureProviderMock.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(featureProviderMock.Object); + var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); @@ -371,8 +370,8 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() featureProviderMock.Setup(x => x.GetProviderHooks()) .Returns(ImmutableList.Empty); - OpenFeature.Instance.SetProvider(featureProviderMock.Object); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.SetProvider(featureProviderMock.Object); + var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); @@ -384,7 +383,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() [Fact] public async Task Should_Use_No_Op_When_Provider_Is_Null() { - OpenFeature.Instance.SetProvider(null); + Api.Instance.SetProvider(null); var client = new FeatureClient("test", "test"); (await client.GetIntegerValue("some-key", 12)).Should().Be(12); } @@ -394,7 +393,7 @@ public void Should_Get_And_Set_Context() { var KEY = "key"; var VAL = 1; - FeatureClient client = OpenFeature.Instance.GetClient(); + FeatureClient client = Api.Instance.GetClient(); client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs similarity index 95% rename from test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs rename to test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 2dc90710..0785be91 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -1,11 +1,11 @@ using System; using AutoFixture; using FluentAssertions; -using OpenFeatureSDK.Model; -using OpenFeatureSDK.Tests.Internal; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class OpenFeatureEvaluationContextTests { diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs similarity index 93% rename from test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs rename to test/OpenFeature.Tests/OpenFeatureHookTests.cs index 3b13e6b8..f854a207 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -6,12 +6,12 @@ using AutoFixture; using FluentAssertions; using Moq; -using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Model; -using OpenFeatureSDK.Tests.Internal; +using OpenFeature.Constant; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture { @@ -79,9 +79,9 @@ public async Task Hooks_Should_Be_Called_In_Order() var testProvider = new TestProvider(); testProvider.AddHook(providerHook.Object); - OpenFeature.Instance.AddHooks(apiHook.Object); - OpenFeature.Instance.SetProvider(testProvider); - var client = OpenFeature.Instance.GetClient(clientName, clientVersion); + Api.Instance.AddHooks(apiHook.Object); + Api.Instance.SetProvider(testProvider); + var client = Api.Instance.GetClient(clientName, clientVersion); client.AddHooks(clientHook.Object); await client.GetBooleanValue(flagName, defaultValue, EvaluationContext.Empty, @@ -182,7 +182,7 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() x.Before(hookContext, It.IsAny>())) .ReturnsAsync(evaluationContext); - var client = OpenFeature.Instance.GetClient("test", "1.0.0"); + var client = Api.Instance.GetClient("test", "1.0.0"); await client.GetBooleanValue("test", false, EvaluationContext.Empty, new FlagEvaluationOptions(ImmutableList.Create(hook1.Object, hook2.Object), ImmutableDictionary.Empty)); @@ -206,7 +206,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() var propHook = "4.3.4hook"; // setup a cascade of overwriting properties - OpenFeature.Instance.SetContext(new EvaluationContextBuilder() + Api.Instance.SetContext(new EvaluationContextBuilder() .Set(propGlobal, true) .Set(propGlobalToOverwrite, false) .Build()); @@ -239,14 +239,14 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() provider.Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ResolutionDetails("test", true)); - OpenFeature.Instance.SetProvider(provider.Object); + Api.Instance.SetProvider(provider.Object); var hook = new Mock(MockBehavior.Strict); hook.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) .ReturnsAsync(hookContext); - var client = OpenFeature.Instance.GetClient("test", "1.0.0", null, clientContext); + var client = Api.Instance.GetClient("test", "1.0.0", null, clientContext); await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook.Object), ImmutableDictionary.Empty)); // after proper merging, all properties should equal true @@ -325,8 +325,8 @@ public async Task Hook_Should_Execute_In_Correct_Order() hook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), It.IsAny>())); - OpenFeature.Instance.SetProvider(featureProvider.Object); - var client = OpenFeature.Instance.GetClient(); + Api.Instance.SetProvider(featureProvider.Object); + var client = Api.Instance.GetClient(); client.AddHooks(hook.Object); await client.GetBooleanValue("test", false); @@ -348,14 +348,14 @@ public async Task Register_Hooks_Should_Be_Available_At_All_Levels() var testProvider = new TestProvider(); testProvider.AddHook(hook4.Object); - OpenFeature.Instance.AddHooks(hook1.Object); - OpenFeature.Instance.SetProvider(testProvider); - var client = OpenFeature.Instance.GetClient(); + Api.Instance.AddHooks(hook1.Object); + Api.Instance.SetProvider(testProvider); + var client = Api.Instance.GetClient(); client.AddHooks(hook2.Object); await client.GetBooleanValue("test", false, null, new FlagEvaluationOptions(hook3.Object, ImmutableDictionary.Empty)); - Assert.Single(OpenFeature.Instance.GetHooks()); + Assert.Single(Api.Instance.GetHooks()); client.GetHooks().Count().Should().Be(1); testProvider.GetProviderHooks().Count.Should().Be(1); } @@ -404,8 +404,8 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() x.Finally(It.IsAny>(), null)) .Throws(new Exception()); - OpenFeature.Instance.SetProvider(featureProvider.Object); - var client = OpenFeature.Instance.GetClient(); + Api.Instance.SetProvider(featureProvider.Object); + var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1.Object, hook2.Object }); client.GetHooks().Count().Should().Be(2); @@ -455,8 +455,8 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() x.Error(It.IsAny>(), It.IsAny(), null)) .Returns(Task.CompletedTask); - OpenFeature.Instance.SetProvider(featureProvider1.Object); - var client = OpenFeature.Instance.GetClient(); + Api.Instance.SetProvider(featureProvider1.Object); + var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1.Object, hook2.Object }); await client.GetBooleanValue("test", false); @@ -492,8 +492,8 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ hook2.InSequence(sequence).Setup(x => x.Error(It.IsAny>(), It.IsAny(), null)); - OpenFeature.Instance.SetProvider(featureProvider.Object); - var client = OpenFeature.Instance.GetClient(); + Api.Instance.SetProvider(featureProvider.Object); + var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1.Object, hook2.Object }); await client.GetBooleanValue("test", false); @@ -538,8 +538,8 @@ public async Task Hook_Hints_May_Be_Optional() .Setup(x => x.Finally(It.IsAny>(), defaultEmptyHookHints)) .Returns(Task.CompletedTask); - OpenFeature.Instance.SetProvider(featureProvider.Object); - var client = OpenFeature.Instance.GetClient(); + Api.Instance.SetProvider(featureProvider.Object); + var client = Api.Instance.GetClient(); await client.GetBooleanValue("test", false, config: flagOptions); @@ -572,7 +572,7 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() hook.InSequence(sequence) .Setup(x => x.Finally(It.IsAny>(), null)).Returns(Task.CompletedTask); - var client = OpenFeature.Instance.GetClient(); + var client = Api.Instance.GetClient(); client.AddHooks(hook.Object); var resolvedFlag = await client.GetBooleanValue("test", true); diff --git a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs similarity index 77% rename from test/OpenFeatureSDK.Tests/OpenFeatureTests.cs rename to test/OpenFeature.Tests/OpenFeatureTests.cs index 8a96e84b..f7a6c66f 100644 --- a/test/OpenFeatureSDK.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,12 +1,12 @@ using System.Linq; using FluentAssertions; using Moq; -using OpenFeatureSDK.Constant; -using OpenFeatureSDK.Model; -using OpenFeatureSDK.Tests.Internal; +using OpenFeature.Constant; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { @@ -14,8 +14,8 @@ public class OpenFeatureTests : ClearOpenFeatureInstanceFixture [Specification("1.1.1", "The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")] public void OpenFeature_Should_Be_Singleton() { - var openFeature = OpenFeature.Instance; - var openFeature2 = OpenFeature.Instance; + var openFeature = Api.Instance; + var openFeature2 = Api.Instance; openFeature.Should().BeSameAs(openFeature2); } @@ -24,7 +24,7 @@ public void OpenFeature_Should_Be_Singleton() [Specification("1.1.3", "The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] public void OpenFeature_Should_Add_Hooks() { - var openFeature = OpenFeature.Instance; + var openFeature = Api.Instance; var hook1 = new Mock(MockBehavior.Strict).Object; var hook2 = new Mock(MockBehavior.Strict).Object; var hook3 = new Mock(MockBehavior.Strict).Object; @@ -53,8 +53,8 @@ public void OpenFeature_Should_Add_Hooks() [Specification("1.1.4", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] public void OpenFeature_Should_Get_Metadata() { - OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); - var openFeature = OpenFeature.Instance; + Api.Instance.SetProvider(new NoOpFeatureProvider()); + var openFeature = Api.Instance; var metadata = openFeature.GetProviderMetadata(); metadata.Should().NotBeNull(); @@ -68,7 +68,7 @@ public void OpenFeature_Should_Get_Metadata() [Specification("1.1.5", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] public void OpenFeature_Should_Create_Client(string name = null, string version = null) { - var openFeature = OpenFeature.Instance; + var openFeature = Api.Instance; var client = openFeature.GetClient(name, version); client.Should().NotBeNull(); @@ -81,21 +81,21 @@ public void Should_Set_Given_Context() { var context = EvaluationContext.Empty; - OpenFeature.Instance.SetContext(context); + Api.Instance.SetContext(context); - OpenFeature.Instance.GetContext().Should().BeSameAs(context); + Api.Instance.GetContext().Should().BeSameAs(context); context = EvaluationContext.Builder().Build(); - OpenFeature.Instance.SetContext(context); + Api.Instance.SetContext(context); - OpenFeature.Instance.GetContext().Should().BeSameAs(context); + Api.Instance.GetContext().Should().BeSameAs(context); } [Fact] public void Should_Always_Have_Provider() { - OpenFeature.Instance.GetProvider().Should().NotBeNull(); + Api.Instance.GetProvider().Should().NotBeNull(); } } } diff --git a/test/OpenFeatureSDK.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs similarity index 95% rename from test/OpenFeatureSDK.Tests/StructureTests.cs rename to test/OpenFeature.Tests/StructureTests.cs index 12bde7fb..c781b83b 100644 --- a/test/OpenFeatureSDK.Tests/StructureTests.cs +++ b/test/OpenFeature.Tests/StructureTests.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; -using FluentAssertions; -using OpenFeatureSDK.Model; +using OpenFeature.Model; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class StructureTests { diff --git a/test/OpenFeatureSDK.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs similarity index 95% rename from test/OpenFeatureSDK.Tests/TestImplementations.cs rename to test/OpenFeature.Tests/TestImplementations.cs index 2da88c7b..b54462f3 100644 --- a/test/OpenFeatureSDK.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; -using OpenFeatureSDK.Model; +using OpenFeature.Model; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class TestHookNoOverride : Hook { } diff --git a/test/OpenFeatureSDK.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs similarity index 95% rename from test/OpenFeatureSDK.Tests/ValueTests.cs rename to test/OpenFeature.Tests/ValueTests.cs index 8c3ae37d..4540618b 100644 --- a/test/OpenFeatureSDK.Tests/ValueTests.cs +++ b/test/OpenFeature.Tests/ValueTests.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using OpenFeatureSDK.Model; +using OpenFeature.Model; using Xunit; -namespace OpenFeatureSDK.Tests +namespace OpenFeature.Tests { public class ValueTests { diff --git a/test/OpenFeatureSDK.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeatureSDK.Tests/ClearOpenFeatureInstanceFixture.cs deleted file mode 100644 index b15b2c30..00000000 --- a/test/OpenFeatureSDK.Tests/ClearOpenFeatureInstanceFixture.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace OpenFeatureSDK.Tests -{ - public class ClearOpenFeatureInstanceFixture - { - // Make sure the singleton is cleared between tests - public ClearOpenFeatureInstanceFixture() - { - OpenFeature.Instance.SetContext(null); - OpenFeature.Instance.ClearHooks(); - OpenFeature.Instance.SetProvider(new NoOpFeatureProvider()); - } - } -} From 80c2e1eca2bf4493ba8af672ccbc02a45282ca19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 15 Oct 2022 21:12:02 -0400 Subject: [PATCH 051/316] chore(main): release 0.5.0 (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [0.5.0](https://github.com/open-feature/dotnet-sdk/compare/v0.4.0...v0.5.0) (2022-10-16) ### ⚠ BREAKING CHANGES * rename OpenFeature class to API and ns to OpenFeature (#82) ### Features * rename OpenFeature class to API and ns to OpenFeature ([#82](https://github.com/open-feature/dotnet-sdk/issues/82)) ([6090bd9](https://github.com/open-feature/dotnet-sdk/commit/6090bd971817cc6cc8b74487b2850d8e99a2c94d)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 11 +++++++++++ build/Common.prod.props | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99e..2aca35ae 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "0.5.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 069eac81..542b88aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.5.0](https://github.com/open-feature/dotnet-sdk/compare/v0.4.0...v0.5.0) (2022-10-16) + + +### ⚠ BREAKING CHANGES + +* rename OpenFeature class to API and ns to OpenFeature (#82) + +### Features + +* rename OpenFeature class to API and ns to OpenFeature ([#82](https://github.com/open-feature/dotnet-sdk/issues/82)) ([6090bd9](https://github.com/open-feature/dotnet-sdk/commit/6090bd971817cc6cc8b74487b2850d8e99a2c94d)) + ## [0.4.0](https://github.com/open-feature/dotnet-sdk/compare/v0.3.0...v0.4.0) (2022-10-12) diff --git a/build/Common.prod.props b/build/Common.prod.props index c721ae7e..c8777399 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.4.0 + 0.5.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 69e44568c4ec0200478dbc4ffc4de4e782b77e6e Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 19 Oct 2022 12:08:17 -0400 Subject: [PATCH 052/316] chore: add docs link (#86) Add link to docs. Signed-off-by: Todd Baert --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 24bd9811..9202ae7c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ var evaluationContext = EvaluationContext.Builder() var isEnabled = await client.GetBooleanValue("my-conditional", false, evaluationContext); ``` +For complete documentation, visit: https://docs.openfeature.dev/docs/category/concepts + ### Provider To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in an existing contrib repository available under the OpenFeature organization. Finally, you’ll then need to write the provider itself. In most languages, this can be accomplished by implementing the provider interface exported by the OpenFeature SDK. From 79c0d8d0aa07f7aa69023de3437c3774df507e53 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 21 Oct 2022 16:14:50 -0400 Subject: [PATCH 053/316] chore: release 1.0.0 (#85) Release-As: 1.0.0 From a6363bd9b1af2d0329f81fd58ab1d4eabe33f7fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 20:37:27 +1000 Subject: [PATCH 054/316] chore(main): release 1.0.0 (#87) :robot: I have created a release *beep* *boop* --- ## [1.0.0](https://github.com/open-feature/dotnet-sdk/compare/v0.5.0...v1.0.0) (2022-10-21) ### Miscellaneous Chores * release 1.0.0 ([#85](https://github.com/open-feature/dotnet-sdk/issues/85)) ([79c0d8d](https://github.com/open-feature/dotnet-sdk/commit/79c0d8d0aa07f7aa69023de3437c3774df507e53)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ build/Common.prod.props | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2aca35ae..fea34540 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.5.0" + ".": "1.0.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 542b88aa..cb367a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.0.0](https://github.com/open-feature/dotnet-sdk/compare/v0.5.0...v1.0.0) (2022-10-21) + + +### Miscellaneous Chores + +* release 1.0.0 ([#85](https://github.com/open-feature/dotnet-sdk/issues/85)) ([79c0d8d](https://github.com/open-feature/dotnet-sdk/commit/79c0d8d0aa07f7aa69023de3437c3774df507e53)) + ## [0.5.0](https://github.com/open-feature/dotnet-sdk/compare/v0.4.0...v0.5.0) (2022-10-16) diff --git a/build/Common.prod.props b/build/Common.prod.props index c8777399..a7a32c0d 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 0.5.0 + 1.0.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 9443239adeb3144c6f683faf400dddf5ac493628 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sat, 29 Oct 2022 05:56:31 +1000 Subject: [PATCH 055/316] fix: correct version range on logging (#89) ## This PR When other dependencies are referring to Microsoft.Extensions.Logging library above 6.0 nuget will be unable to resolve and cause a mismatch issue. We should support anything above 2.0 ### Related Issues ### Notes ### Follow-up Tasks ### How to test Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- build/Common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Common.props b/build/Common.props index 49ac8adf..4c9a3d69 100644 --- a/build/Common.props +++ b/build/Common.props @@ -18,7 +18,7 @@ Please sort alphabetically. Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax. --> - [2.0,6.0) + [2.0,) [1.0.0,2.0) From 4071b8e8b10dd635f92015aa18ae3742f6a77f93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Oct 2022 06:00:14 +1000 Subject: [PATCH 056/316] chore(main): release 1.0.1 (#90) :robot: I have created a release *beep* *boop* --- ## [1.0.1](https://github.com/open-feature/dotnet-sdk/compare/v1.0.0...v1.0.1) (2022-10-28) ### Bug Fixes * correct version range on logging ([#89](https://github.com/open-feature/dotnet-sdk/issues/89)) ([9443239](https://github.com/open-feature/dotnet-sdk/commit/9443239adeb3144c6f683faf400dddf5ac493628)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ build/Common.prod.props | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fea34540..a8f71224 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "1.0.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cb367a85..38f751b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.0.1](https://github.com/open-feature/dotnet-sdk/compare/v1.0.0...v1.0.1) (2022-10-28) + + +### Bug Fixes + +* correct version range on logging ([#89](https://github.com/open-feature/dotnet-sdk/issues/89)) ([9443239](https://github.com/open-feature/dotnet-sdk/commit/9443239adeb3144c6f683faf400dddf5ac493628)) + ## [1.0.0](https://github.com/open-feature/dotnet-sdk/compare/v0.5.0...v1.0.0) (2022-10-21) diff --git a/build/Common.prod.props b/build/Common.prod.props index a7a32c0d..5124a9b0 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 1.0.0 + 1.0.1 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From 428e1ce0cf6aac735f2d7b21687c64759382e935 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 21:49:12 +1000 Subject: [PATCH 057/316] chore: configure Renovate (#88) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} From 880344f977bdc2658e42b94812f360c45498c4f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 21:53:14 +1000 Subject: [PATCH 058/316] chore(deps): update dependency dotnet-sdk to v6.0.402 (#91) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 69fd3584..f62ff544 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "6.0.100" + "version": "6.0.402" } } From ba40c7513deb6c0e774bffe469806b78fd0bf42f Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Wed, 2 Nov 2022 07:58:25 +1000 Subject: [PATCH 059/316] chore: remove dependabot now we use renovate (#93) Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> ## This PR - remove dependabot now we use renovate - https://github.com/open-feature/dotnet-sdk/pull/88 ### Related Issues ### Notes ### Follow-up Tasks ### How to test Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .github/dependabot.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f1a4f656..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - labels: - - "dependency" From 1612dc4ebd4128a845bf24ae79b90f8bc8c526d7 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 10 Nov 2022 14:46:09 -0800 Subject: [PATCH 060/316] chore: Update README basic usage. (#96) --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9202ae7c..a9e4c4a1 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,14 @@ The packages will aim to support all current .NET versions. Refer to the current ### Basic Usage ```csharp -using OpenFeature; +using OpenFeature.Model; // Sets the provider used by the client -Api.Instance.SetProvider(new NoOpProvider()); +// If no provider is set, then a default NoOpProvider will be used. +//OpenFeature.Api.Instance.SetProvider(new MyProvider()); + // Gets a instance of the feature flag client -var client = OpenFeature.Instance.GetClient(); +var client = OpenFeature.Api.Instance.GetClient(); // Evaluation the `my-feature` feature flag var isEnabled = await client.GetBooleanValue("my-feature", false); From 594d5f21f735473bf8585f9f6de67d758b1bf12c Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sat, 12 Nov 2022 20:35:10 +1000 Subject: [PATCH 061/316] feat: include net7 in the test suit (#97) Since .net 7 was released this week, we should also run our test suite across it Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .github/workflows/code-coverage.yml | 7 ++++++- .github/workflows/dotnet-format.yml | 4 ++-- .github/workflows/linux-ci.yml | 9 ++++++++- .github/workflows/windows-ci.yml | 9 ++++++++- global.json | 2 +- test/OpenFeature.Tests/OpenFeature.Tests.csproj | 2 +- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 07742a62..7d1d026a 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - version: [net6.0] + version: [net7.0] env: OS: ${{ matrix.os }} @@ -26,6 +26,11 @@ jobs: with: fetch-depth: 0 + - name: Setup .NET Core 7.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0.x' + - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index f1fbef59..5616a74d 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -20,10 +20,10 @@ jobs: - name: Check out code uses: actions/checkout@v3 - - name: Setup .NET Core 6.0 + - name: Setup .NET Core 7.0 uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Install format tool run: dotnet tool install -g dotnet-format diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index ddad7023..1394677a 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -16,13 +16,20 @@ jobs: strategy: matrix: - version: [net6.0] + version: [net6.0,net7.0] steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Setup .NET Core 6.0.x, 7.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 11119320..1542290d 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -16,13 +16,20 @@ jobs: strategy: matrix: - version: [net462,net6.0] + version: [net462,net6.0,net7.0] steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Setup .NET Core 6.0.x, 7.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + - name: Install dependencies run: dotnet restore diff --git a/global.json b/global.json index f62ff544..40b47077 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "6.0.402" + "version": "7.0.100" } } diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index c620e976..7f630ef0 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net7.0 $(TargetFrameworks);net462 OpenFeature.Tests From 9e67a9f9624ff4d344bba89168f82c5d7e7f001a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Jan 2023 16:55:10 -0500 Subject: [PATCH 062/316] chore(deps): update dependency dotnet-sdk to v7.0.101 (#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dotnet-sdk](https://togithub.com/dotnet/sdk) | dotnet-sdk | patch | `7.0.100` -> `7.0.101` | --- ### Release Notes
dotnet/sdk ### [`v7.0.101`](https://togithub.com/dotnet/sdk/releases/tag/v7.0.101): .NET SDK 7.0.101 [Release](https://togithub.com/dotnet/core/releases/tag/v7.0.1)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 40b47077..0f417661 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.100" + "version": "7.0.101" } } From 7cc7ab46fc20a97c9f4398f6d1fe80e43db514e1 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 13 Jan 2023 08:57:29 -0500 Subject: [PATCH 063/316] feat: add STATIC, CACHED reasons (#101) As per: https://github.com/open-feature/spec/blob/7ed7442c40d4e766424671258846b21a96adda70/specification/types.md#resolution-details Signed-off-by: Todd Baert --- src/OpenFeature/Constant/Reason.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/OpenFeature/Constant/Reason.cs b/src/OpenFeature/Constant/Reason.cs index 8e233d5e..8d08651d 100644 --- a/src/OpenFeature/Constant/Reason.cs +++ b/src/OpenFeature/Constant/Reason.cs @@ -26,6 +26,16 @@ public static class Reason ///
public static string Default = "DEFAULT"; + /// + /// The resolved value is static (no dynamic evaluation) + /// + public static string Static = "STATIC"; + + /// + /// The resolved value was retrieved from cache + /// + public static 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 From 9cf4154a1cf29b0142d033e8d0d12c6cbe0b85c4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:08:43 -0500 Subject: [PATCH 064/316] chore(deps): update dependency dotnet-sdk to v7.0.102 (#100) --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 0f417661..29ca966f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.101" + "version": "7.0.102" } } From 5a09c4f38c15b47b6e1aa62a57ea4f49c08fab77 Mon Sep 17 00:00:00 2001 From: Chris Donnelly Date: Tue, 17 Jan 2023 15:20:58 -0600 Subject: [PATCH 065/316] feat: Make IFeatureClient interface public. (#102) Signed-off-by: Chris Donnelly --- src/OpenFeature/IFeatureClient.cs | 130 ++++++++++++++++++++++++++- src/OpenFeature/OpenFeatureClient.cs | 128 ++++---------------------- 2 files changed, 144 insertions(+), 114 deletions(-) diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index f1fd4ad8..2186ca68 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -4,24 +4,152 @@ namespace OpenFeature { - internal interface IFeatureClient + /// + /// Interface used to resolve flags of varying types. + /// + public interface IFeatureClient { + /// + /// 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(); + /// + /// Resolves a boolean feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag value. Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + + /// + /// Resolves a boolean feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag details Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + /// + /// Resolves a string feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag value. Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + + /// + /// Resolves a string feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag details Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + /// + /// Resolves a integer feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag value. Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + + /// + /// Resolves a integer feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag details Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + /// + /// Resolves a double feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag value. Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + + /// + /// Resolves a double feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag details Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + /// + /// Resolves a structure object feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag value. Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + + /// + /// Resolves a structure object feature flag + /// + /// Feature flag key + /// Default value + /// Evaluation Context + /// Flag Evaluation Options + /// Resolved flag details Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); } } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index f33fb1a7..115f7510 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -53,15 +53,7 @@ public sealed class FeatureClient : IFeatureClient return (method(provider), provider); } - /// - /// Gets the EvaluationContext 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 + /// public EvaluationContext GetContext() { lock (this._evaluationContextLock) @@ -70,10 +62,7 @@ public EvaluationContext GetContext() } } - /// - /// Sets the EvaluationContext of the client - /// - /// The to set + /// public void SetContext(EvaluationContext context) { lock (this._evaluationContextLock) @@ -97,10 +86,7 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat this._evaluationContext = context ?? EvaluationContext.Empty; } - /// - /// Gets client metadata - /// - /// Client metadata + /// public ClientMetadata GetMetadata() => this._metadata; /// @@ -113,24 +99,10 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// Hook that implements the interface public void AddHooks(Hook hook) => this._hooks.Push(hook); - /// - /// Appends hooks to client - /// - /// The appending operation will be atomic. - /// - /// - /// A list of Hooks that implement the interface + /// public void AddHooks(IEnumerable hooks) => this._hooks.PushRange(hooks.ToArray()); - /// - /// 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(); /// @@ -138,131 +110,61 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// public void ClearHooks() => this._hooks.Clear(); - /// - /// Resolves a boolean feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => (await this.GetBooleanDetails(flagKey, defaultValue, context, config)).Value; - /// - /// Resolves a boolean feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue), FlagValueType.Boolean, flagKey, defaultValue, context, config); - /// - /// Resolves a string feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => (await this.GetStringDetails(flagKey, defaultValue, context, config)).Value; - /// - /// Resolves a string feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue), FlagValueType.String, flagKey, defaultValue, context, config); - /// - /// Resolves a integer feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => (await this.GetIntegerDetails(flagKey, defaultValue, context, config)).Value; - /// - /// Resolves a integer feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue), FlagValueType.Number, flagKey, defaultValue, context, config); - /// - /// Resolves a double feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => (await this.GetDoubleDetails(flagKey, defaultValue, context, config)).Value; - /// - /// Resolves a double feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue), FlagValueType.Number, flagKey, defaultValue, context, config); - /// - /// Resolves a structure object feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => (await this.GetObjectDetails(flagKey, defaultValue, context, config)).Value; - /// - /// Resolves a structure object feature flag - /// - /// Feature flag key - /// Default value - /// Evaluation Context - /// Flag Evaluation Options - /// Resolved flag details + /// public async Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue), From 851900c22fe84f9dbb0de74d879ecb66387f25e0 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 18 Jan 2023 11:50:55 -0500 Subject: [PATCH 066/316] chore: fix release flow, add version.txt Signed-off-by: Todd Baert --- .github/workflows/release.yml | 8 ++++++++ version.txt | 1 + 2 files changed, 9 insertions(+) create mode 100644 version.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 048160b2..74e5990e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,14 @@ jobs: with: fetch-depth: 0 + - name: Setup .NET Core 6.0.x, 7.0.x + if: ${{ steps.release.outputs.releases_created }} + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + - name: Install dependencies if: ${{ steps.release.outputs.releases_created }} run: dotnet restore diff --git a/version.txt b/version.txt new file mode 100644 index 00000000..7f207341 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +1.0.1 \ No newline at end of file From 6d820cbaf4a240f6977f0dc7596b52c63d6fbd8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Jan 2023 11:57:11 -0500 Subject: [PATCH 067/316] chore(main): release 1.1.0 (#107) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ build/Common.prod.props | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a8f71224..2601677b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.1" + ".": "1.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f751b8..88a4df09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.1.0](https://github.com/open-feature/dotnet-sdk/compare/v1.0.1...v1.1.0) (2023-01-18) + + +### Features + +* add STATIC, CACHED reasons ([#101](https://github.com/open-feature/dotnet-sdk/issues/101)) ([7cc7ab4](https://github.com/open-feature/dotnet-sdk/commit/7cc7ab46fc20a97c9f4398f6d1fe80e43db514e1)) +* include net7 in the test suit ([#97](https://github.com/open-feature/dotnet-sdk/issues/97)) ([594d5f2](https://github.com/open-feature/dotnet-sdk/commit/594d5f21f735473bf8585f9f6de67d758b1bf12c)) +* Make IFeatureClient interface public. ([#102](https://github.com/open-feature/dotnet-sdk/issues/102)) ([5a09c4f](https://github.com/open-feature/dotnet-sdk/commit/5a09c4f38c15b47b6e1aa62a57ea4f49c08fab77)) + ## [1.0.1](https://github.com/open-feature/dotnet-sdk/compare/v1.0.0...v1.0.1) (2022-10-28) diff --git a/build/Common.prod.props b/build/Common.prod.props index 5124a9b0..15e7c42f 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 1.0.1 + 1.1.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. From bd730a52424214c1c917a80cf2cee03d0e4aafa8 Mon Sep 17 00:00:00 2001 From: Chris Donnelly Date: Wed, 25 Jan 2023 13:35:04 -0600 Subject: [PATCH 068/316] docs: fix broken links (#109) Signed-off-by: Chris Donnelly --- README.md | 2 +- src/OpenFeature/Api.cs | 2 +- src/OpenFeature/Constant/ErrorType.cs | 2 +- src/OpenFeature/Constant/Reason.cs | 2 +- src/OpenFeature/FeatureProvider.cs | 2 +- src/OpenFeature/Hook.cs | 2 +- src/OpenFeature/Model/EvaluationContext.cs | 2 +- .../Model/FlagEvaluationDetails.cs | 2 +- .../Model/FlagEvaluationOptions.cs | 2 +- src/OpenFeature/Model/HookContext.cs | 2 +- src/OpenFeature/Model/ResolutionDetails.cs | 2 +- .../OpenFeature.Tests/FeatureProviderTests.cs | 33 ++++------ .../OpenFeatureClientTests.cs | 8 +-- .../OpenFeatureEvaluationContextTests.cs | 6 +- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 60 ++++++++++++++++++- test/OpenFeature.Tests/OpenFeatureTests.cs | 4 +- 16 files changed, 88 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index a9e4c4a1..d8aa461b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OpenFeature SDK for .NET [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -[![spec version badge](https://img.shields.io/badge/Specification-v0.5.0-yellow)](https://github.com/open-feature/spec/tree/v0.5.0?rgh-link-date=2022-09-27T17%3A53%3A52Z) +[![spec version badge](https://img.shields.io/badge/Specification-v0.5.2-yellow)](https://github.com/open-feature/spec/tree/v0.5.2?rgh-link-date=2023-01-20T21%3A37%3A52Z) [![codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) [![nuget](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://bestpractices.coreinfrastructure.org/projects/6250) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index b01d7b94..ba3fea3b 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -11,7 +11,7 @@ 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 { private EvaluationContext _evaluationContext = EvaluationContext.Empty; diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs index 496eed72..232a57cb 100644 --- a/src/OpenFeature/Constant/ErrorType.cs +++ b/src/OpenFeature/Constant/ErrorType.cs @@ -5,7 +5,7 @@ namespace OpenFeature.Constant /// /// These errors are used to indicate abnormal execution when evaluation a flag /// - /// + /// public enum ErrorType { /// diff --git a/src/OpenFeature/Constant/Reason.cs b/src/OpenFeature/Constant/Reason.cs index 8d08651d..a60ce78a 100644 --- a/src/OpenFeature/Constant/Reason.cs +++ b/src/OpenFeature/Constant/Reason.cs @@ -3,7 +3,7 @@ namespace OpenFeature.Constant /// /// Common reasons used during flag resolution /// - /// Reason Specification + /// Reason Specification public static class Reason { /// diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 3209e810..fe8f664d 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -8,7 +8,7 @@ namespace OpenFeature /// The provider interface describes the abstraction layer for a feature flag provider. /// A provider acts as the translates layer between the generic feature flag structure to a target feature flag system. /// - /// Provider specification + /// Provider specification public abstract class FeatureProvider { /// diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs index a4fc4065..c35c3cb4 100644 --- a/src/OpenFeature/Hook.cs +++ b/src/OpenFeature/Hook.cs @@ -18,7 +18,7 @@ namespace OpenFeature /// Hooks can be configured to run globally (impacting all flag evaluations), per client, or per flag evaluation invocation. /// /// - /// Hook Specification + /// Hook Specification public abstract class Hook { /// diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs index ccdcbac7..e8a94bc9 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -8,7 +8,7 @@ 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 + /// Evaluation context public sealed class EvaluationContext { private readonly Structure _structure; diff --git a/src/OpenFeature/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs index d2c92c80..3871d306 100644 --- a/src/OpenFeature/Model/FlagEvaluationDetails.cs +++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs @@ -6,7 +6,7 @@ namespace OpenFeature.Model /// The contract returned to the caller that describes the result of the flag evaluation process. /// /// Flag value type - /// + /// public class FlagEvaluationDetails { /// diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs index b3a3b196..92e4f355 100644 --- a/src/OpenFeature/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs @@ -6,7 +6,7 @@ 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 + /// Flag Evaluation Options public class FlagEvaluationOptions { /// diff --git a/src/OpenFeature/Model/HookContext.cs b/src/OpenFeature/Model/HookContext.cs index f67b3b0c..edb2d93d 100644 --- a/src/OpenFeature/Model/HookContext.cs +++ b/src/OpenFeature/Model/HookContext.cs @@ -7,7 +7,7 @@ namespace OpenFeature.Model /// Context provided to hook execution /// /// Flag value type - /// + /// public class HookContext { /// diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs index 1e8f882d..66094410 100644 --- a/src/OpenFeature/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -7,7 +7,7 @@ namespace OpenFeature.Model /// Describes the details of the feature flag being evaluated /// /// Flag value type - /// + /// public class ResolutionDetails { /// diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index c050f31d..ba159a97 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -12,8 +12,7 @@ namespace OpenFeature.Tests public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { [Fact] - [Specification("2.1", - "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")] + [Specification("2.1.1", "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")] public void Provider_Must_Have_Metadata() { var provider = new TestProvider(); @@ -22,22 +21,14 @@ public void Provider_Must_Have_Metadata() } [Fact] - [Specification("2.2", - "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `flag resolution` structure.")] - [Specification("2.3.1", - "The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.")] - [Specification("2.4", - "In cases of normal execution, the `provider` MUST populate the `flag resolution` structure's `value` field with the resolved flag value.")] - [Specification("2.5", - "In cases of normal execution, the `provider` SHOULD populate the `flag resolution` structure's `variant` field with a string identifier corresponding to the returned flag value.")] - [Specification("2.6", - "The `provider` SHOULD populate the `flag resolution` structure's `reason` field with a string indicating the semantic reason for the returned flag value.")] - [Specification("2.7", - "In cases of normal execution, the `provider` MUST NOT populate the `flag resolution` structure's `error code` field, or otherwise must populate it with a null or falsy value.")] - [Specification("2.9.1", - "The `flag resolution` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] - [Specification("2.11", - "In cases of normal execution, the `provider` MUST NOT populate the `flag resolution` structure's `error message` field, or otherwise must populate it with a null or falsy value.")] + [Specification("2.2.1", "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.")] + [Specification("2.2.2.1", "The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.")] + [Specification("2.2.3", "In cases of normal execution, the `provider` MUST populate the `resolution details` structure's `value` field with the resolved flag value.")] + [Specification("2.2.4", "In cases of normal execution, the `provider` SHOULD populate the `resolution details` structure's `variant` field with a string identifier corresponding to the returned flag value.")] + [Specification("2.2.5", "The `provider` SHOULD populate the `resolution details` structure's `reason` field with `\"STATIC\"`, `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"CACHED\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.")] + [Specification("2.2.6", "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error code` field, or otherwise must populate it with a null or falsy value.")] + [Specification("2.2.8.1", "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] + [Specification("2.3.2", "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error message` field, or otherwise must populate it with a null or falsy value.")] public async Task Provider_Must_Resolve_Flag_Values() { var fixture = new Fixture(); @@ -76,10 +67,8 @@ public async Task Provider_Must_Resolve_Flag_Values() } [Fact] - [Specification("2.8", - "In cases of abnormal execution, the `provider` MUST indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")] - [Specification("2.12", - "In cases of abnormal execution, the `evaluation details` structure's `error message` field MAY contain a string containing additional detail about the nature of the error.")] + [Specification("2.2.7", "In cases of abnormal execution, the `provider` MUST indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")] + [Specification("2.3.3", "In cases of abnormal execution, the `resolution details` structure's `error message` field MAY contain a string containing additional detail about the nature of the error.")] public async Task Provider_Must_ErrorType() { var fixture = new Fixture(); diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 87ec10e5..b5249f6b 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -56,7 +56,7 @@ public void OpenFeatureClient_Metadata_Should_Have_Name() [Fact] [Specification("1.3.1", "The `client` MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns the flag value.")] - [Specification("1.3.2.1", "he client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")] + [Specification("1.3.2.1", "The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")] [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() { @@ -103,7 +103,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() [Specification("1.4.5", "In cases of normal execution, the `evaluation details` structure's `variant` field MUST contain the value of the `variant` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")] [Specification("1.4.6", "In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")] [Specification("1.4.11", "The `client` SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")] - [Specification("2.9", "The `flag resolution` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] + [Specification("2.2.8.1", "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() { var fixture = new Fixture(); @@ -147,9 +147,9 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() } [Fact] - [Specification("1.1.2", "The API MUST provide a function to set the global provider singleton, which accepts an API-conformant provider implementation.")] + [Specification("1.1.2", "The `API` MUST provide a function to set the global `provider` singleton, which accepts an API-conformant `provider` implementation.")] [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] - [Specification("1.4.7", "In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST contain a string identifying an error occurred during flag evaluation and the nature of the error.")] + [Specification("1.4.7", "In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST contain an `error code`.")] [Specification("1.4.8", "In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")] [Specification("1.4.9", "Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")] [Specification("1.4.10", "In the case of abnormal execution, the client SHOULD log an informative error message.")] diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 0785be91..a9906cf4 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -25,7 +25,7 @@ public void Should_Merge_Two_Contexts() } [Fact] - [Specification("3.2.2", "Duplicate values being overwritten.")] + [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.")] public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Context() { var contextBuilder1 = new EvaluationContextBuilder(); @@ -43,8 +43,8 @@ public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Cont } [Fact] - [Specification("3.1", "The `evaluation context` structure MUST define an optional `targeting key` field of type string, identifying the subject of the flag evaluation.")] - [Specification("3.2", "The evaluation context MUST support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | datetime | structure`.")] + [Specification("3.1.1", "The `evaluation context` structure MUST define an optional `targeting key` field of type string, identifying the subject of the flag evaluation.")] + [Specification("3.1.2", "The evaluation context MUST support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | datetime | structure`.")] public void EvaluationContext_Should_All_Types() { var fixture = new Fixture(); diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index f854a207..df127790 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -17,7 +17,7 @@ public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture { [Fact] [Specification("1.5.1", "The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")] - [Specification("2.10", "The provider interface MUST define a provider hook mechanism which can be optionally implemented in order to add hook instances to the evaluation life-cycle.")] + [Specification("2.3.1", "The provider interface MUST define a `provider hook` mechanism which can be optionally implemented in order to add `hook` instances to the evaluation life-cycle.")] [Specification("4.4.2", "Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API")] public async Task Hooks_Should_Be_Called_In_Order() { @@ -191,7 +191,8 @@ await client.GetBooleanValue("test", false, EvaluationContext.Empty, } [Fact] - [Specification("4.3.4", "When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context in the following order: before-hook (highest precedence), invocation, client, api (lowest precedence).")] + [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`.")] public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() { var propGlobal = "4.3.4global"; @@ -265,7 +266,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() [Specification("4.2.1", "`hook hints` MUST be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`..")] [Specification("4.2.2.1", "Condition: `Hook hints` MUST be immutable.")] [Specification("4.2.2.2", "Condition: The client `metadata` field in the `hook context` MUST be immutable.")] - [Specification("4.2.2.3", "Condition: The provider `metadata` field in the `hook context` MUST be immutable")] + [Specification("4.2.2.3", "Condition: The provider `metadata` field in the `hook context` MUST be immutable.")] [Specification("4.3.1", "Hooks MUST specify at least one stage.")] public async Task Hook_Should_Return_No_Errors() { @@ -550,6 +551,7 @@ public async Task Hook_Hints_May_Be_Optional() } [Fact] + [Specification("4.4.5", "If an error occurs in the `before` or `after` hooks, the `error` hooks MUST be invoked.")] [Specification("4.4.7", "If an error occurs in the `before` hooks, the default value MUST be returned.")] public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() { @@ -582,5 +584,57 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() hook.Verify(x => x.Error(It.IsAny>(), exceptionToThrow, null), Times.Once); hook.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); } + + [Fact] + [Specification("4.4.5", "If an error occurs in the `before` or `after` hooks, the `error` hooks MUST be invoked.")] + public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() + { + var featureProvider = new Mock(MockBehavior.Strict); + var hook = new Mock(MockBehavior.Strict); + var defaultEmptyHookHints = new Dictionary(); + var flagOptions = new FlagEvaluationOptions(hook.Object); + var exceptionToThrow = new Exception("Fails during default"); + EvaluationContext evaluationContext = null; + + var sequence = new MockSequence(); + + featureProvider.Setup(x => x.GetMetadata()) + .Returns(new Metadata(null)); + + featureProvider.Setup(x => x.GetProviderHooks()) + .Returns(ImmutableList.Empty); + + hook.InSequence(sequence) + .Setup(x => x.Before(It.IsAny>(), defaultEmptyHookHints)) + .ReturnsAsync(evaluationContext); + + featureProvider.InSequence(sequence) + .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new ResolutionDetails("test", false)); + + hook.InSequence(sequence) + .Setup(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints)) + .ThrowsAsync(exceptionToThrow); + + hook.InSequence(sequence) + .Setup(x => x.Error(It.IsAny>(), It.IsAny(), defaultEmptyHookHints)) + .Returns(Task.CompletedTask); + + hook.InSequence(sequence) + .Setup(x => x.Finally(It.IsAny>(), defaultEmptyHookHints)) + .Returns(Task.CompletedTask); + + Api.Instance.SetProvider(featureProvider.Object); + var client = Api.Instance.GetClient(); + + var resolvedFlag = await client.GetBooleanValue("test", true, config: flagOptions); + + resolvedFlag.Should().BeTrue(); + hook.Verify(x => x.Before(It.IsAny>(), defaultEmptyHookHints), Times.Once); + hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints), Times.Once); + hook.Verify(x => x.Error(It.IsAny>(), exceptionToThrow, defaultEmptyHookHints), Times.Once); + hook.Verify(x => x.Finally(It.IsAny>(), defaultEmptyHookHints), Times.Once); + featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index f7a6c66f..5292fa82 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -11,7 +11,7 @@ namespace OpenFeature.Tests public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { [Fact] - [Specification("1.1.1", "The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")] + [Specification("1.1.1", "The `API`, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.")] public void OpenFeature_Should_Be_Singleton() { var openFeature = Api.Instance; @@ -21,7 +21,7 @@ public void OpenFeature_Should_Be_Singleton() } [Fact] - [Specification("1.1.3", "The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] + [Specification("1.1.3", "The `API` MUST provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] public void OpenFeature_Should_Add_Hooks() { var openFeature = Api.Instance; From 5f348f46f2d9a5578a0db951bd78508ab74cabc0 Mon Sep 17 00:00:00 2001 From: Valentinas <31697821+valentk777@users.noreply.github.com> Date: Mon, 13 Feb 2023 19:46:13 +0200 Subject: [PATCH 069/316] feat: split errors to classes by types (#115) Signed-off-by: Valentinas <31697821+valentk777@users.noreply.github.com> --- .../Error/FeatureProviderException.cs | 4 ++-- .../Error/FlagNotFoundException.cs | 21 +++++++++++++++++++ src/OpenFeature/Error/GeneralException.cs | 21 +++++++++++++++++++ .../Error/InvalidContextException.cs | 21 +++++++++++++++++++ src/OpenFeature/Error/ParseErrorException.cs | 21 +++++++++++++++++++ .../Error/ProviderNotReadyException.cs | 21 +++++++++++++++++++ .../Error/TargetingKeyMissingException.cs | 21 +++++++++++++++++++ .../Error/TypeMismatchException.cs | 21 +++++++++++++++++++ 8 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/OpenFeature/Error/FlagNotFoundException.cs create mode 100644 src/OpenFeature/Error/GeneralException.cs create mode 100644 src/OpenFeature/Error/InvalidContextException.cs create mode 100644 src/OpenFeature/Error/ParseErrorException.cs create mode 100644 src/OpenFeature/Error/ProviderNotReadyException.cs create mode 100644 src/OpenFeature/Error/TargetingKeyMissingException.cs create mode 100644 src/OpenFeature/Error/TypeMismatchException.cs diff --git a/src/OpenFeature/Error/FeatureProviderException.cs b/src/OpenFeature/Error/FeatureProviderException.cs index d5c7743a..df74afa4 100644 --- a/src/OpenFeature/Error/FeatureProviderException.cs +++ b/src/OpenFeature/Error/FeatureProviderException.cs @@ -4,8 +4,8 @@ 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 + /// 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 { diff --git a/src/OpenFeature/Error/FlagNotFoundException.cs b/src/OpenFeature/Error/FlagNotFoundException.cs new file mode 100644 index 00000000..6e33a48f --- /dev/null +++ b/src/OpenFeature/Error/FlagNotFoundException.cs @@ -0,0 +1,21 @@ +using System; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// + /// Provider was unable to find the flag error when evaluating a flag. + /// + public class FlagNotFoundException : FeatureProviderException + { + /// + /// 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 new file mode 100644 index 00000000..d99c9077 --- /dev/null +++ b/src/OpenFeature/Error/GeneralException.cs @@ -0,0 +1,21 @@ +using System; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// + /// Abnormal execution of the provider when evaluating a flag. + /// + public class GeneralException : FeatureProviderException + { + /// + /// 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 new file mode 100644 index 00000000..798a513e --- /dev/null +++ b/src/OpenFeature/Error/InvalidContextException.cs @@ -0,0 +1,21 @@ +using System; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// + /// Context does not satisfy provider requirements when evaluating a flag. + /// + public class InvalidContextException : FeatureProviderException + { + /// + /// 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 new file mode 100644 index 00000000..5d9a7d73 --- /dev/null +++ b/src/OpenFeature/Error/ParseErrorException.cs @@ -0,0 +1,21 @@ +using System; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// + /// Provider failed to parse the flag response when evaluating a flag. + /// + public class ParseErrorException : FeatureProviderException + { + /// + /// 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/ProviderNotReadyException.cs b/src/OpenFeature/Error/ProviderNotReadyException.cs new file mode 100644 index 00000000..b3a8ef9f --- /dev/null +++ b/src/OpenFeature/Error/ProviderNotReadyException.cs @@ -0,0 +1,21 @@ +using System; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// + /// Provider has yet been initialized when evaluating a flag. + /// + public class ProviderNotReadyException : FeatureProviderException + { + /// + /// 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 new file mode 100644 index 00000000..9a69fe9b --- /dev/null +++ b/src/OpenFeature/Error/TargetingKeyMissingException.cs @@ -0,0 +1,21 @@ +using System; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// + /// Context does not contain a targeting key and the provider requires one when evaluating a flag. + /// + public class TargetingKeyMissingException : FeatureProviderException + { + /// + /// 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 new file mode 100644 index 00000000..4f224753 --- /dev/null +++ b/src/OpenFeature/Error/TypeMismatchException.cs @@ -0,0 +1,21 @@ +using System; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// + /// Request type does not match the expected type when evaluating a flag. + /// + public class TypeMismatchException : FeatureProviderException + { + /// + /// 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) + { + } + } +} From d0ae94cb5fdd99e4701fcac6e512cbdf622f0ccd Mon Sep 17 00:00:00 2001 From: Valentinas <31697821+valentk777@users.noreply.github.com> Date: Tue, 14 Feb 2023 16:13:07 +0200 Subject: [PATCH 070/316] test: add initial api benchmarks (#116) Signed-off-by: Valentinas <31697821+valentk777@users.noreply.github.com> --- CONTRIBUTING.md | 12 ++ OpenFeature.sln | 32 ++++-- build/Common.tests.props | 1 + src/OpenFeature/OpenFeature.csproj | 3 + .../OpenFeature.Benchmarks.csproj | 18 +++ .../OpenFeatureClientBenchmarks.cs | 105 ++++++++++++++++++ test/OpenFeature.Benchmarks/Program.cs | 12 ++ 7 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj create mode 100644 test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs create mode 100644 test/OpenFeature.Benchmarks/Program.cs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba1064a4..70c6d321 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,3 +119,15 @@ As with other OpenFeature SDKs, dotnet-sdk follows the This project includes a [`.editorconfig`](./.editorconfig) file which is supported by all the IDEs/editor mentioned above. It works with the IDE/editor only and does not affect the actual build of the project. + +## Benchmarking + +We use [BenchmarkDotNet](https://benchmarkdotnet.org/articles/overview.html) NuGet package to benchmark a code. + +To run pipelines locally, you can follow these commands from a root project directory. + +``` +dotnet restore +dotnet build --configuration Release --output "./release" --no-restore +dotnet release/OpenFeature.Benchmarks.dll +``` diff --git a/OpenFeature.sln b/OpenFeature.sln index 9b75048f..195c6410 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -1,21 +1,24 @@ ο»Ώ Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}" ProjectSection(SolutionItems) = preProject - build\Common.props = build\Common.props - build\Common.tests.props = build\Common.tests.props - build\Common.prod.props = build\Common.prod.props .github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + build\Common.prod.props = build\Common.prod.props + build\Common.props = build\Common.props + build\Common.tests.props = build\Common.tests.props + CONTRIBUTING.md = CONTRIBUTING.md .github\workflows\dotnet-format.yml = .github\workflows\dotnet-format.yml + .github\workflows\lint-pr.yml = .github\workflows\lint-pr.yml .github\workflows\linux-ci.yml = .github\workflows\linux-ci.yml + README.md = README.md .github\workflows\release.yml = .github\workflows\release.yml .github\workflows\windows-ci.yml = .github\workflows\windows-ci.yml - README.md = README.md - CONTRIBUTING.md = CONTRIBUTING.md - .github\workflows\lint-pr.yml = .github\workflows\lint-pr.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C97E9975-E10A-4817-AE2C-4DD69C3C02D4}" @@ -29,7 +32,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{65FBA159-2 test\Directory.Build.props = test\Directory.Build.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Tests", "test\OpenFeature.Tests\OpenFeature.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\OpenFeature.Tests\OpenFeature.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,9 +50,20 @@ Global {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.Build.0 = Debug|Any CPU {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.ActiveCfg = Release|Any CPU {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.Build.0 = Release|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} EndGlobalSection EndGlobal diff --git a/build/Common.tests.props b/build/Common.tests.props index d4a6454e..675f4e50 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -21,6 +21,7 @@ Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax. --> [4.17.0] + [0.13.1] [3.1.2] [6.7.0] [17.2.0] diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index 77a03baf..c00781c4 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -10,6 +10,9 @@ + + <_Parameter1>OpenFeature.Benchmarks + <_Parameter1>OpenFeature.Tests diff --git a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj new file mode 100644 index 00000000..25e613b7 --- /dev/null +++ b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + OpenFeature.Benchmark + Exe + + + + + + + + + + + + diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs new file mode 100644 index 00000000..a17bfb8b --- /dev/null +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -0,0 +1,105 @@ + +using System.Collections.Immutable; +using System.Threading.Tasks; +using AutoFixture; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using OpenFeature.Model; + +namespace OpenFeature.Benchmark +{ + [MemoryDiagnoser] + [SimpleJob(RuntimeMoniker.Net60, baseline: true)] + [JsonExporterAttribute.Full] + [JsonExporterAttribute.FullCompressed] + public class OpenFeatureClientBenchmarks + { + private readonly string _clientName; + private readonly string _clientVersion; + private readonly string _flagName; + private readonly bool _defaultBoolValue; + private readonly string _defaultStringValue; + private readonly int _defaultIntegerValue; + private readonly double _defaultDoubleValue; + private readonly Value _defaultStructureValue; + private readonly FlagEvaluationOptions _emptyFlagOptions; + private readonly FeatureClient _client; + + public OpenFeatureClientBenchmarks() + { + var fixture = new Fixture(); + _clientName = fixture.Create(); + _clientVersion = fixture.Create(); + _flagName = fixture.Create(); + _defaultBoolValue = fixture.Create(); + _defaultStringValue = fixture.Create(); + _defaultIntegerValue = fixture.Create(); + _defaultDoubleValue = fixture.Create(); + _defaultStructureValue = fixture.Create(); + _emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); + + Api.Instance.SetProvider(new NoOpFeatureProvider()); + _client = Api.Instance.GetClient(_clientName, _clientVersion); + } + + [Benchmark] + public async Task OpenFeatureClient_GetBooleanValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetBooleanValue(_flagName, _defaultBoolValue); + + [Benchmark] + public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetBooleanValue(_flagName, _defaultBoolValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await _client.GetBooleanValue(_flagName, _defaultBoolValue, EvaluationContext.Empty, _emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetStringValue(_flagName, _defaultStringValue); + + [Benchmark] + public async Task OpenFeatureClient_GetStringValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetStringValue(_flagName, _defaultStringValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithEmptyFlagEvaluationOptions() => + await _client.GetStringValue(_flagName, _defaultStringValue, EvaluationContext.Empty, _emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetIntegerValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetIntegerValue(_flagName, _defaultIntegerValue); + + [Benchmark] + public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetIntegerValue(_flagName, _defaultIntegerValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await _client.GetIntegerValue(_flagName, _defaultIntegerValue, EvaluationContext.Empty, _emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetDoubleValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetDoubleValue(_flagName, _defaultDoubleValue); + + [Benchmark] + public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetDoubleValue(_flagName, _defaultDoubleValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await _client.GetDoubleValue(_flagName, _defaultDoubleValue, EvaluationContext.Empty, _emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetObjectValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetObjectValue(_flagName, _defaultStructureValue); + + [Benchmark] + public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await _client.GetObjectValue(_flagName, _defaultStructureValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await _client.GetObjectValue(_flagName, _defaultStructureValue, EvaluationContext.Empty, _emptyFlagOptions); + } +} diff --git a/test/OpenFeature.Benchmarks/Program.cs b/test/OpenFeature.Benchmarks/Program.cs new file mode 100644 index 00000000..0738b272 --- /dev/null +++ b/test/OpenFeature.Benchmarks/Program.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Running; + +namespace OpenFeature.Benchmark +{ + internal class Program + { + static void Main(string[] args) + { + BenchmarkRunner.Run(); + } + } +} From 83603f70a97c6efdb9349d32240a2b56b3a7d50f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:17:56 -0500 Subject: [PATCH 071/316] chore(main): release 1.2.0 (#117) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ build/Common.prod.props | 2 +- version.txt | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2601677b..d0ab6645 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.1.0" + ".": "1.2.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a4df09..6ed0d87e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.2.0](https://github.com/open-feature/dotnet-sdk/compare/v1.1.0...v1.2.0) (2023-02-14) + + +### Features + +* split errors to classes by types ([#115](https://github.com/open-feature/dotnet-sdk/issues/115)) ([5f348f4](https://github.com/open-feature/dotnet-sdk/commit/5f348f46f2d9a5578a0db951bd78508ab74cabc0)) + ## [1.1.0](https://github.com/open-feature/dotnet-sdk/compare/v1.0.1...v1.1.0) (2023-01-18) diff --git a/build/Common.prod.props b/build/Common.prod.props index 15e7c42f..4032dcd5 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 1.1.0 + 1.2.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 7f207341..26aaba0e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.1 \ No newline at end of file +1.2.0 From 5b8969ec2bcb1599ebcea4e9ce55d0e02d1d3ef8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:47:13 -0400 Subject: [PATCH 072/316] chore(deps): update dependency dotnet-sdk to v7.0.202 (#118) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 29ca966f..6579ed13 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.102" + "version": "7.0.202" } } From 6cd2b70264bb12e5e7bb88e9d26fe974632efb1b Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 23 Mar 2023 15:33:34 -0400 Subject: [PATCH 073/316] chore: add CODEOWNERS Signed-off-by: Todd Baert --- CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..9436647d --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,6 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence +# +# Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-dotnet/workgroup.yaml +# +* @open-feature/sdk-dotnet-maintainers From 43d64dcea330aaf8776b56d383eb4c988ace82bd Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Fri, 14 Apr 2023 16:49:27 -0400 Subject: [PATCH 074/316] update link to use new doc domain --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8aa461b..e15451ab 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ var evaluationContext = EvaluationContext.Builder() var isEnabled = await client.GetBooleanValue("my-conditional", false, evaluationContext); ``` -For complete documentation, visit: https://docs.openfeature.dev/docs/category/concepts +For complete documentation, visit: https://openfeature.dev/docs/category/concepts ### Provider From b19e6e540b43ed8d3fca4b19b85034cacc25da37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Apr 2023 09:17:39 -0700 Subject: [PATCH 075/316] chore(deps): update codecov/codecov-action action to v3.1.2 (#120) 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 7d1d026a..e72967b0 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -40,7 +40,7 @@ jobs: - name: Test ${{ matrix.version }} run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v3.1.1 + - uses: codecov/codecov-action@v3.1.2 with: env_vars: OS name: Code Coverage for ${{ matrix.os }} From 4a2e8ec118d1a64c030e697dd243645a7e649198 Mon Sep 17 00:00:00 2001 From: odubajDT <93584209+odubajDT@users.noreply.github.com> Date: Tue, 25 Apr 2023 15:26:03 +0200 Subject: [PATCH 076/316] docs: adapt README according to general template (#122) Signed-off-by: odubajDT Co-authored-by: Florian Bacher --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e15451ab..08a38f2a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ -# OpenFeature SDK for .NET + +

+ + + + OpenFeature Logo + +

+ +

OpenFeature .NET SDK

[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![spec version badge](https://img.shields.io/badge/Specification-v0.5.2-yellow)](https://github.com/open-feature/spec/tree/v0.5.2?rgh-link-date=2023-01-20T21%3A37%3A52Z) @@ -6,15 +15,49 @@ [![nuget](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://bestpractices.coreinfrastructure.org/projects/6250) -OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. +## πŸ‘‹ Hey there! Thanks for checking out the OpenFeature .NET SDK + +### What is OpenFeature? + +[OpenFeature][openfeature-website] is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. + +### Why standardize feature flags? + +Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition. + +## πŸ” Requirements: + +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ + +Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 + -## Supported .Net Versions +## πŸ“¦ Installation: -The packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) Excluding .NET Framework 3.5 +Use the following to initialize your project: -## Getting Started +```sh +dotnet new console +``` + +and install OpenFeature: + +```sh +dotnet add package OpenFeature +``` + +## 🌟 Features: -### Basic Usage +- support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider) +- easy integration and extension via [hooks](https://openfeature.dev/docs/reference/concepts/hooks) +- bool, string, numeric and object flag types +- [context-aware](https://openfeature.dev/docs/reference/concepts/evaluation-context) evaluation + +## πŸš€ Usage: + +### Basics: ```csharp using OpenFeature.Model; @@ -27,6 +70,20 @@ using OpenFeature.Model; var client = OpenFeature.Api.Instance.GetClient(); // Evaluation the `my-feature` feature flag var isEnabled = await client.GetBooleanValue("my-feature", false); +``` + +For complete documentation, visit: https://openfeature.dev/docs/category/concepts + +### Context-aware evaluation: + +Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server. +In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting). +If the flag system you're using supports targeting, you can provide the input data using the `EvaluationContext`. + +```csharp +using OpenFeature.Model; + +var client = OpenFeature.Api.Instance.GetClient(); // Evaluating with a context. var evaluationContext = EvaluationContext.Builder() @@ -37,13 +94,9 @@ var evaluationContext = EvaluationContext.Builder() var isEnabled = await client.GetBooleanValue("my-conditional", false, evaluationContext); ``` -For complete documentation, visit: https://openfeature.dev/docs/category/concepts +### Providers: -### Provider - -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in an existing contrib repository available under the OpenFeature organization. Finally, you’ll then need to write the provider itself. In most languages, this can be accomplished by implementing the provider interface exported by the OpenFeature SDK. - -Example of implementing a feature flag provider +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. Finally, you’ll then need to write the provider itself. This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. ```csharp using OpenFeature; @@ -90,11 +143,11 @@ public class MyFeatureProvider : FeatureProvider } ``` -### Hook +See [here](https://openfeature.dev/docs/reference/technologies/server/dotnet) for a catalog of available providers. -Hooks are a mechanism that allow for the addition of arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validation of the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking. +### Hooks: -Example of adding a hook +Hooks are a mechanism that allow for the addition of arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validation of the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking. ```csharp // add a hook globally, to run on all evaluations @@ -138,14 +191,26 @@ public class MyHook : Hook } ``` -## Contributing +See [here](https://openfeature.dev/docs/reference/technologies/server/dotnet) for a catalog of available hooks. -See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to the OpenFeature project. +### Logging: -Our community meetings are held regularly and open to everyone. Check the [OpenFeature community calendar](https://calendar.google.com/calendar/u/0?cid=MHVhN2kxaGl2NWRoMThiMjd0b2FoNjM2NDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) for specific dates and for the Zoom meeting links. +The .NET SDK uses Microsoft Extensions Logger. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. +## ⭐️ Support the project -Thanks so much for your contributions to the OpenFeature project. +- Give this repo a ⭐️! +- Follow us social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more check out our [community page](https://openfeature.dev/community/) + +## 🀝 Contributing + +Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. + +### Thanks to everyone that has already contributed @@ -153,6 +218,8 @@ Thanks so much for your contributions to the OpenFeature project. Made with [contrib.rocks](https://contrib.rocks). -## License +## πŸ“œ License [Apache License 2.0](LICENSE) + +[openfeature-website]: https://openfeature.dev From 8478c9202825a47596e961ab3b3c3b70efb6b265 Mon Sep 17 00:00:00 2001 From: odubajDT <93584209+odubajDT@users.noreply.github.com> Date: Thu, 27 Apr 2023 20:56:52 +0200 Subject: [PATCH 077/316] test: introduce BDD e2e tests (#124) Signed-off-by: odubajDT --- .github/workflows/ci.yml | 46 +++ .github/workflows/code-coverage.yml | 15 +- .github/workflows/e2e.yml | 39 +++ .github/workflows/linux-ci.yml | 40 --- .github/workflows/windows-ci.yml | 40 --- .gitignore | 4 + .gitmodules | 3 + CONTRIBUTING.md | 36 ++- OpenFeature.sln | 2 + src/OpenFeature/OpenFeature.csproj | 3 + test-harness | 1 + test/OpenFeature.E2ETests/Features/.gitkeep | 0 .../OpenFeature.E2ETests.csproj | 38 +++ .../Steps/EvaluationStepDefinitions.cs | 275 ++++++++++++++++++ 14 files changed, 443 insertions(+), 99 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/e2e.yml delete mode 100644 .github/workflows/linux-ci.yml delete mode 100644 .github/workflows/windows-ci.yml create mode 100644 .gitmodules create mode 160000 test-harness create mode 100644 test/OpenFeature.E2ETests/Features/.gitkeep create mode 100644 test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj create mode 100644 test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d76b1ff7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: Test + +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' + pull_request: + branches: [ main ] + paths-ignore: + - '**.md' + +jobs: + unit-tests-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup .NET Core 6.0.x, 7.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + + - name: Run Tests + run: dotnet test test/OpenFeature.Tests/ --configuration Release --logger:"console;verbosity=detailed" + + unit-tests-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup .NET Core 6.0.x, 7.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + + - name: Run Tests + run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger:"console;verbosity=detailed" diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index e72967b0..3e3d9bc8 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -13,11 +13,6 @@ on: jobs: build-test-report: runs-on: ubuntu-latest - - strategy: - matrix: - version: [net7.0] - env: OS: ${{ matrix.os }} @@ -31,14 +26,8 @@ jobs: with: dotnet-version: '7.0.x' - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true - - - name: Test ${{ matrix.version }} - run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + - name: Run Test + run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - uses: codecov/codecov-action@v3.1.2 with: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..336bd702 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,39 @@ +name: E2E Test + +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' + pull_request: + branches: [ main ] + paths-ignore: + - '**.md' + +jobs: + e2e-tests: + runs-on: ubuntu-latest + services: + flagd: + image: ghcr.io/open-feature/flagd-testbed:latest + ports: + - 8013:8013 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup .NET Core 6.0.x, 7.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + + - name: Initialize Tests + run: | + git submodule update --init --recursive + cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ + + - name: Run Tests + run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger:"console;verbosity=detailed" diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml deleted file mode 100644 index 1394677a..00000000 --- a/.github/workflows/linux-ci.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Linux - -on: - push: - branches: [ main ] - paths-ignore: - - '**.md' - pull_request: - branches: [ main ] - paths-ignore: - - '**.md' - -jobs: - build-test: - runs-on: ubuntu-latest - - strategy: - matrix: - version: [net6.0,net7.0] - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Setup .NET Core 6.0.x, 7.0.x - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 7.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - - name: Test ${{ matrix.version }} - run: dotnet test **/bin/**/${{ matrix.version }}/*.Tests.dll --configuration Release --no-build --logger:"console;verbosity=detailed" diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml deleted file mode 100644 index 1542290d..00000000 --- a/.github/workflows/windows-ci.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Windows - -on: - push: - branches: [ main ] - paths-ignore: - - '**.md' - pull_request: - branches: [ main ] - paths-ignore: - - '**.md' - -jobs: - build-test: - runs-on: windows-latest - - strategy: - matrix: - version: [net462,net6.0,net7.0] - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Setup .NET Core 6.0.x, 7.0.x - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 7.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - - name: Test ${{ matrix.version }} - run: dotnet test **\bin\**\${{ matrix.version }}\*Tests.dll --configuration Release --no-build --logger:"console;verbosity=detailed" diff --git a/.gitignore b/.gitignore index cb35ed59..575a49ad 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,7 @@ ASALocalRun/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json + +# integration tests +test/OpenFeature.E2ETests/Features/evaluation.feature +test/OpenFeature.E2ETests/Features/evaluation.feature.cs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..61d2eb45 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test-harness"] + path = test-harness + url = https://github.com/open-feature/test-harness.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70c6d321..77eef66a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,12 +38,6 @@ Add your fork as an origin git remote add fork https://github.com/YOUR_GITHUB_USERNAME/dotnet-sdk.git ``` -Makes sure your development environment is all setup by building and testing -```bash -dotnet build -dotnet test -``` - To start working on a new feature or bugfix, create a new branch and start working on it. ```bash @@ -55,6 +49,36 @@ git push fork feat/NAME_OF_FEATURE Open a pull request against the main dotnet-sdk repository. +### Running tests locally + +#### Unit tests + +To run unit tests execute: + +```bash +dotnet test test/OpenFeature.Tests/ +``` + +#### E2E tests + +To be able to run the e2e tests, first we need to initialize the submodule and copy the test files: + +```bash +git submodule update --init --recursive && cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ +``` + +Afterwards, you need to start flagd locally: + +```bash +docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest +``` + +Now you can run the tests using: + +```bash +dotnet test test/OpenFeature.E2ETests/ +``` + ### How to Receive Comments * If the PR is not ready for review, please mark it as diff --git a/OpenFeature.sln b/OpenFeature.sln index 195c6410..5ed0e809 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -36,6 +36,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index c00781c4..e04d063d 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -16,6 +16,9 @@ <_Parameter1>OpenFeature.Tests + + <_Parameter1>OpenFeature.E2ETests +
diff --git a/test-harness b/test-harness new file mode 160000 index 00000000..01c4a433 --- /dev/null +++ b/test-harness @@ -0,0 +1 @@ +Subproject commit 01c4a433a3bcb0df6448da8c0f8030d11ce710af diff --git a/test/OpenFeature.E2ETests/Features/.gitkeep b/test/OpenFeature.E2ETests/Features/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj new file mode 100644 index 00000000..5368701e --- /dev/null +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -0,0 +1,38 @@ + + + + net6.0;net7.0 + $(TargetFrameworks);net462 + OpenFeature.E2ETests + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs new file mode 100644 index 00000000..bb177468 --- /dev/null +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using OpenFeature.Constant; +using OpenFeature.Contrib.Providers.Flagd; +using OpenFeature.Model; +using TechTalk.SpecFlow; +using Xunit; + +namespace OpenFeature.E2ETests +{ + [Binding] + public class EvaluationStepDefinitions + { + private readonly ScenarioContext _scenarioContext; + private static FeatureClient client; + private Task booleanFlagValue; + private Task stringFlagValue; + private Task intFlagValue; + private Task doubleFlagValue; + private Task objectFlagValue; + private Task> booleanFlagDetails; + private Task> stringFlagDetails; + private Task> intFlagDetails; + private Task> doubleFlagDetails; + private Task> objectFlagDetails; + private string contextAwareFlagKey; + private string contextAwareDefaultValue; + private string contextAwareValue; + private EvaluationContext context; + private string notFoundFlagKey; + private string notFoundDefaultValue; + private FlagEvaluationDetails notFoundDetails; + private string typeErrorFlagKey; + private int typeErrorDefaultValue; + private FlagEvaluationDetails typeErrorDetails; + + public EvaluationStepDefinitions(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + var flagdProvider = new FlagdProvider(); + Api.Instance.SetProvider(flagdProvider); + client = Api.Instance.GetClient(); + } + + [Given(@"a provider is registered with cache disabled")] + public void Givenaproviderisregisteredwithcachedisabled() + { + + } + + [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] + public void Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) + { + this.booleanFlagValue = client.GetBooleanValue(flagKey, defaultValue); + } + + [Then(@"the resolved boolean value should be ""(.*)""")] + public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) + { + Assert.Equal(expectedValue, this.booleanFlagValue.Result); + } + + [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] + public void Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) + { + this.stringFlagValue = client.GetStringValue(flagKey, defaultValue); + } + + [Then(@"the resolved string value should be ""(.*)""")] + public void Thentheresolvedstringvalueshouldbe(string expected) + { + Assert.Equal(expected, this.stringFlagValue.Result); + } + + [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] + public void Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) + { + this.intFlagValue = client.GetIntegerValue(flagKey, defaultValue); + } + + [Then(@"the resolved integer value should be (.*)")] + public void Thentheresolvedintegervalueshouldbe(int expected) + { + Assert.Equal(expected, this.intFlagValue.Result); + } + + [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] + public void Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) + { + this.doubleFlagValue = client.GetDoubleValue(flagKey, defaultValue); + } + + [Then(@"the resolved float value should be (.*)")] + public void Thentheresolvedfloatvalueshouldbe(double expected) + { + Assert.Equal(expected, this.doubleFlagValue.Result); + } + + [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] + public void Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) + { + this.objectFlagValue = client.GetObjectValue(flagKey, new Value()); + } + + [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] + public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) + { + Value value = this.objectFlagValue.Result; + Assert.Equal(boolValue, value.AsStructure[boolField].AsBoolean); + Assert.Equal(stringValue, value.AsStructure[stringField].AsString); + Assert.Equal(numberValue, value.AsStructure[numberField].AsInteger); + } + + [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] + public void Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) + { + this.booleanFlagDetails = client.GetBooleanDetails(flagKey, defaultValue); + } + + [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(bool expectedValue, string expectedVariant, string expectedReason) + { + var result = this.booleanFlagDetails.Result; + Assert.Equal(expectedValue, result.Value); + Assert.Equal(expectedVariant, result.Variant); + Assert.Equal(expectedReason, result.Reason); + } + + [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] + public void Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) + { + this.stringFlagDetails = client.GetStringDetails(flagKey, defaultValue); + } + + [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(string expectedValue, string expectedVariant, string expectedReason) + { + var result = this.stringFlagDetails.Result; + Assert.Equal(expectedValue, result.Value); + Assert.Equal(expectedVariant, result.Variant); + Assert.Equal(expectedReason, result.Reason); + } + + [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] + public void Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) + { + this.intFlagDetails = client.GetIntegerDetails(flagKey, defaultValue); + } + + [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(int expectedValue, string expectedVariant, string expectedReason) + { + var result = this.intFlagDetails.Result; + Assert.Equal(expectedValue, result.Value); + Assert.Equal(expectedVariant, result.Variant); + Assert.Equal(expectedReason, result.Reason); + } + + [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] + public void Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) + { + this.doubleFlagDetails = client.GetDoubleDetails(flagKey, defaultValue); + } + + [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(double expectedValue, string expectedVariant, string expectedReason) + { + var result = this.doubleFlagDetails.Result; + Assert.Equal(expectedValue, result.Value); + Assert.Equal(expectedVariant, result.Variant); + Assert.Equal(expectedReason, result.Reason); + } + + [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] + public void Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) + { + this.objectFlagDetails = client.GetObjectDetails(flagKey, new Value()); + } + + [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] + public void Thentheresolvedobjectdetailsvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) + { + Value value = this.objectFlagDetails.Result.Value; + Assert.Equal(boolValue, value.AsStructure[boolField].AsBoolean); + Assert.Equal(stringValue, value.AsStructure[stringField].AsString); + Assert.Equal(numberValue, value.AsStructure[numberField].AsInteger); + } + + [Then(@"the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Giventhevariantshouldbeandthereasonshouldbe(string expectedVariant, string expectedReason) + { + Assert.Equal(expectedVariant, this.objectFlagDetails.Result.Variant); + Assert.Equal(expectedReason, this.objectFlagDetails.Result.Reason); + } + + [When(@"context contains keys ""(.*)"", ""(.*)"", ""(.*)"", ""(.*)"" with values ""(.*)"", ""(.*)"", (.*), ""(.*)""")] + public void Whencontextcontainskeyswithvalues(string field1, string field2, string field3, string field4, string value1, string value2, int value3, string value4) + { + var attributes = ImmutableDictionary.CreateBuilder(); + attributes.Add(field1, new Value(value1)); + attributes.Add(field2, new Value(value2)); + attributes.Add(field3, new Value(value3)); + attributes.Add(field4, new Value(bool.Parse(value4))); + this.context = new EvaluationContext(new Structure(attributes)); + } + + [When(@"a flag with key ""(.*)"" is evaluated with default value ""(.*)""")] + public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) + { + contextAwareFlagKey = flagKey; + contextAwareDefaultValue = defaultValue; + contextAwareValue = client.GetStringValue(flagKey, contextAwareDefaultValue, context).Result; + } + + [Then(@"the resolved string response should be ""(.*)""")] + public void Thentheresolvedstringresponseshouldbe(string expected) + { + Assert.Equal(expected, this.contextAwareValue); + } + + [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] + public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) + { + string emptyContextValue = client.GetStringValue(contextAwareFlagKey, contextAwareDefaultValue, new EvaluationContext(new Structure(ImmutableDictionary.Empty))).Result; + Assert.Equal(expected, emptyContextValue); + } + + [When(@"a non-existent string flag with key ""(.*)"" is evaluated with details and a default value ""(.*)""")] + public void Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultvalue(string flagKey, string defaultValue) + { + this.notFoundFlagKey = flagKey; + this.notFoundDefaultValue = defaultValue; + this.notFoundDetails = client.GetStringDetails(this.notFoundFlagKey, this.notFoundDefaultValue).Result; + } + + [Then(@"the default string value should be returned")] + public void Thenthedefaultstringvalueshouldbereturned() + { + Assert.Equal(this.notFoundDefaultValue, this.notFoundDetails.Value); + } + + [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] + public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) + { + Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason); + Assert.Contains(errorCode, notFoundDetails.ErrorMessage); + } + + [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] + public void Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultvalue(string flagKey, int defaultValue) + { + this.typeErrorFlagKey = flagKey; + this.typeErrorDefaultValue = defaultValue; + this.typeErrorDetails = client.GetIntegerDetails(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; + } + + [Then(@"the default integer value should be returned")] + public void Thenthedefaultintegervalueshouldbereturned() + { + Assert.Equal(this.typeErrorDefaultValue, this.typeErrorDetails.Value); + } + + [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] + public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) + { + Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason); + Assert.Contains(errorCode, this.typeErrorDetails.ErrorMessage); + } + + } +} From adfdf5038aa75c2ae4c0f909b21e0e69e165be28 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 May 2023 09:54:19 +1000 Subject: [PATCH 078/316] chore(deps): update codecov/codecov-action action to v3.1.3 (#123) 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 3e3d9bc8..3e983141 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -29,7 +29,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v3.1.2 + - uses: codecov/codecov-action@v3.1.3 with: env_vars: OS name: Code Coverage for ${{ matrix.os }} From 1d99726abc6f9b4d6d36edffd6e9e67742990230 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 May 2023 09:56:59 +1000 Subject: [PATCH 079/316] chore(deps): update dependency dotnet-sdk to v7.0.203 (#121) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 6579ed13..549ca2d9 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.202" + "version": "7.0.203" } } From 6cab2d5c0ae09f0d0fef971a07777820f3ad075a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 09:47:37 +1000 Subject: [PATCH 080/316] chore(deps): update codecov/codecov-action action to v3.1.4 (#125) 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 3e983141..75ee00db 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -29,7 +29,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v3.1.3 + - uses: codecov/codecov-action@v3.1.4 with: env_vars: OS name: Code Coverage for ${{ matrix.os }} From 3c5a63386066cbe79086db1d16ffaa8156473d50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 19:50:25 +1000 Subject: [PATCH 081/316] chore(deps): update dependency dotnet-sdk to v7.0.302 (#128) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 549ca2d9..4b6743ee 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.203" + "version": "7.0.302" } } From 9152d631d4268bd3ce22f5075c287e26ee3d9b59 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Wed, 31 May 2023 04:38:07 +1000 Subject: [PATCH 082/316] chore: Exclude standard error from code coverage (#130) Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- src/OpenFeature/Error/FlagNotFoundException.cs | 2 ++ src/OpenFeature/Error/GeneralException.cs | 2 ++ src/OpenFeature/Error/InvalidContextException.cs | 2 ++ src/OpenFeature/Error/ParseErrorException.cs | 2 ++ src/OpenFeature/Error/ProviderNotReadyException.cs | 2 ++ src/OpenFeature/Error/TargetingKeyMissingException.cs | 2 ++ src/OpenFeature/Error/TypeMismatchException.cs | 2 ++ 7 files changed, 14 insertions(+) diff --git a/src/OpenFeature/Error/FlagNotFoundException.cs b/src/OpenFeature/Error/FlagNotFoundException.cs index 6e33a48f..6b8fb4bb 100644 --- a/src/OpenFeature/Error/FlagNotFoundException.cs +++ b/src/OpenFeature/Error/FlagNotFoundException.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; namespace OpenFeature.Error @@ -6,6 +7,7 @@ namespace OpenFeature.Error /// /// Provider was unable to find the flag error when evaluating a flag. /// + [ExcludeFromCodeCoverage] public class FlagNotFoundException : FeatureProviderException { /// diff --git a/src/OpenFeature/Error/GeneralException.cs b/src/OpenFeature/Error/GeneralException.cs index d99c9077..42e0fe73 100644 --- a/src/OpenFeature/Error/GeneralException.cs +++ b/src/OpenFeature/Error/GeneralException.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; namespace OpenFeature.Error @@ -6,6 +7,7 @@ namespace OpenFeature.Error /// /// Abnormal execution of the provider when evaluating a flag. /// + [ExcludeFromCodeCoverage] public class GeneralException : FeatureProviderException { /// diff --git a/src/OpenFeature/Error/InvalidContextException.cs b/src/OpenFeature/Error/InvalidContextException.cs index 798a513e..6bcc8051 100644 --- a/src/OpenFeature/Error/InvalidContextException.cs +++ b/src/OpenFeature/Error/InvalidContextException.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; namespace OpenFeature.Error @@ -6,6 +7,7 @@ namespace OpenFeature.Error /// /// Context does not satisfy provider requirements when evaluating a flag. /// + [ExcludeFromCodeCoverage] public class InvalidContextException : FeatureProviderException { /// diff --git a/src/OpenFeature/Error/ParseErrorException.cs b/src/OpenFeature/Error/ParseErrorException.cs index 5d9a7d73..7b3d21e9 100644 --- a/src/OpenFeature/Error/ParseErrorException.cs +++ b/src/OpenFeature/Error/ParseErrorException.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; namespace OpenFeature.Error @@ -6,6 +7,7 @@ namespace OpenFeature.Error /// /// Provider failed to parse the flag response when evaluating a flag. /// + [ExcludeFromCodeCoverage] public class ParseErrorException : FeatureProviderException { /// diff --git a/src/OpenFeature/Error/ProviderNotReadyException.cs b/src/OpenFeature/Error/ProviderNotReadyException.cs index b3a8ef9f..c3c8b5d0 100644 --- a/src/OpenFeature/Error/ProviderNotReadyException.cs +++ b/src/OpenFeature/Error/ProviderNotReadyException.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; namespace OpenFeature.Error @@ -6,6 +7,7 @@ namespace OpenFeature.Error /// /// Provider has yet been initialized when evaluating a flag. /// + [ExcludeFromCodeCoverage] public class ProviderNotReadyException : FeatureProviderException { /// diff --git a/src/OpenFeature/Error/TargetingKeyMissingException.cs b/src/OpenFeature/Error/TargetingKeyMissingException.cs index 9a69fe9b..632cc791 100644 --- a/src/OpenFeature/Error/TargetingKeyMissingException.cs +++ b/src/OpenFeature/Error/TargetingKeyMissingException.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; namespace OpenFeature.Error @@ -6,6 +7,7 @@ namespace OpenFeature.Error /// /// Context does not contain a targeting key and the provider requires one when evaluating a flag. /// + [ExcludeFromCodeCoverage] public class TargetingKeyMissingException : FeatureProviderException { /// diff --git a/src/OpenFeature/Error/TypeMismatchException.cs b/src/OpenFeature/Error/TypeMismatchException.cs index 4f224753..96c23872 100644 --- a/src/OpenFeature/Error/TypeMismatchException.cs +++ b/src/OpenFeature/Error/TypeMismatchException.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; namespace OpenFeature.Error @@ -6,6 +7,7 @@ namespace OpenFeature.Error /// /// Request type does not match the expected type when evaluating a flag. /// + [ExcludeFromCodeCoverage] public class TypeMismatchException : FeatureProviderException { /// From 3f765c6fb4ccd651de2d4f46e1fec38cd26610fe Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Wed, 31 May 2023 21:20:37 +1000 Subject: [PATCH 083/316] feat: Support for name client to given provider (#129) Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- src/OpenFeature/Api.cs | 50 +++++++++-- src/OpenFeature/OpenFeatureClient.cs | 8 +- .../OpenFeatureClientTests.cs | 2 +- test/OpenFeature.Tests/OpenFeatureTests.cs | 82 ++++++++++++++++++- test/OpenFeature.Tests/TestImplementations.cs | 2 +- 5 files changed, 127 insertions(+), 17 deletions(-) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index ba3fea3b..e679bf75 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -15,7 +15,9 @@ namespace OpenFeature public sealed class Api { private EvaluationContext _evaluationContext = EvaluationContext.Empty; - private FeatureProvider _featureProvider = new NoOpFeatureProvider(); + private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); + private readonly ConcurrentDictionary _featureProviders = + new ConcurrentDictionary(); private readonly ConcurrentStack _hooks = new ConcurrentStack(); /// The reader/writer locks are not disposed because the singleton instance should never be disposed. @@ -42,7 +44,7 @@ public void SetProvider(FeatureProvider featureProvider) this._featureProviderLock.EnterWriteLock(); try { - this._featureProvider = featureProvider; + this._defaultProvider = featureProvider ?? this._defaultProvider; } finally { @@ -50,6 +52,17 @@ public void SetProvider(FeatureProvider featureProvider) } } + /// + /// Sets the feature provider to given clientName + /// + /// Name of client + /// Implementation of + public void SetProvider(string clientName, FeatureProvider featureProvider) + { + this._featureProviders.AddOrUpdate(clientName, featureProvider, + (key, current) => featureProvider); + } + /// /// Gets the feature provider /// @@ -57,7 +70,7 @@ public void SetProvider(FeatureProvider featureProvider) /// 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 - /// . + /// . /// /// /// @@ -66,7 +79,7 @@ public FeatureProvider GetProvider() this._featureProviderLock.EnterReadLock(); try { - return this._featureProvider; + return this._defaultProvider; } finally { @@ -74,17 +87,44 @@ public FeatureProvider GetProvider() } } + /// + /// Gets the feature provider with given clientName + /// + /// Name of client + /// A provider associated with the given clientName, if clientName is empty or doesn't + /// have a corresponding provider the default provider will be returned + public FeatureProvider GetProvider(string clientName) + { + if (string.IsNullOrEmpty(clientName)) + { + return this.GetProvider(); + } + + return this._featureProviders.TryGetValue(clientName, out var featureProvider) + ? featureProvider + : this.GetProvider(); + } + + /// /// 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 . + /// For multiple dependent provider operations see . /// /// /// public Metadata GetProviderMetadata() => this.GetProvider().GetMetadata(); + /// + /// Gets providers metadata assigned to the given clientName. If the clientName has no provider + /// assigned to it the default provider will be returned + /// + /// Name of client + /// Metadata assigned to provider + public Metadata GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); + /// /// Create a new instance of using the current provider /// diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 115f7510..38a0fd63 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -42,13 +42,7 @@ public sealed class FeatureClient : IFeatureClient { // Alias the provider reference so getting the method and returning the provider are // guaranteed to be the same object. - var provider = Api.Instance.GetProvider(); - - if (provider == null) - { - provider = new NoOpFeatureProvider(); - this._logger.LogDebug("No provider configured, using no-op provider"); - } + var provider = Api.Instance.GetProvider(this._metadata.Name); return (method(provider), provider); } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index b5249f6b..aec5a631 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -147,7 +147,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() } [Fact] - [Specification("1.1.2", "The `API` MUST provide a function to set the global `provider` singleton, which accepts an API-conformant `provider` implementation.")] + [Specification("1.1.2", "The `API` MUST provide a function to set the default `provider`, which accepts an API-conformant `provider` implementation.")] [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] [Specification("1.4.7", "In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST contain an `error code`.")] [Specification("1.4.8", "In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")] diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 5292fa82..f6ad03a0 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -21,7 +21,65 @@ public void OpenFeature_Should_Be_Singleton() } [Fact] - [Specification("1.1.3", "The `API` MUST provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider() + { + var openFeature = Api.Instance; + + openFeature.SetProvider(new NoOpFeatureProvider()); + openFeature.SetProvider(TestProvider.Name, new TestProvider()); + + var defaultClient = openFeature.GetProviderMetadata(); + var namedClient = openFeature.GetProviderMetadata(TestProvider.Name); + + defaultClient.Name.Should().Be(NoOpProvider.NoOpProviderName); + namedClient.Name.Should().Be(TestProvider.Name); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() + { + var openFeature = Api.Instance; + + openFeature.SetProvider(new TestProvider()); + + var defaultClient = openFeature.GetProviderMetadata(); + + defaultClient.Name.Should().Be(TestProvider.Name); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Assign_Provider_To_Existing_Client() + { + const string name = "new-client"; + var openFeature = Api.Instance; + + openFeature.SetProvider(name, new TestProvider()); + openFeature.SetProvider(name, new NoOpFeatureProvider()); + + openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public void OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance() + { + var openFeature = Api.Instance; + var provider = new TestProvider(); + + openFeature.SetProvider("a", provider); + openFeature.SetProvider("b", provider); + + var clientA = openFeature.GetProvider("a"); + var clientB = openFeature.GetProvider("b"); + + clientA.Should().Be(clientB); + } + + [Fact] + [Specification("1.1.4", "The `API` MUST provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] public void OpenFeature_Should_Add_Hooks() { var openFeature = Api.Instance; @@ -50,7 +108,7 @@ public void OpenFeature_Should_Add_Hooks() } [Fact] - [Specification("1.1.4", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] + [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] public void OpenFeature_Should_Get_Metadata() { Api.Instance.SetProvider(new NoOpFeatureProvider()); @@ -65,7 +123,7 @@ public void OpenFeature_Should_Get_Metadata() [InlineData("client1", "version1")] [InlineData("client2", null)] [InlineData(null, null)] - [Specification("1.1.5", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] + [Specification("1.1.6", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] public void OpenFeature_Should_Create_Client(string name = null, string version = null) { var openFeature = Api.Instance; @@ -97,5 +155,23 @@ public void Should_Always_Have_Provider() { Api.Instance.GetProvider().Should().NotBeNull(); } + + [Fact] + public void OpenFeature_Should_Allow_Multiple_Client_Mapping() + { + var openFeature = Api.Instance; + + openFeature.SetProvider("client1", new TestProvider()); + openFeature.SetProvider("client2", new NoOpFeatureProvider()); + + var client1 = openFeature.GetClient("client1"); + var client2 = openFeature.GetClient("client2"); + + client1.GetMetadata().Name.Should().Be("client1"); + client2.GetMetadata().Name.Should().Be("client2"); + + client1.GetBooleanValue("test", false).Result.Should().BeTrue(); + client2.GetBooleanValue("test", false).Result.Should().BeFalse(); + } } } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index b54462f3..e2bcf5e9 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -50,7 +50,7 @@ public override Metadata GetMetadata() public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); } public override Task> ResolveStringValue(string flagKey, string defaultValue, From 16f3300094b4dececd9bcb00dfb3d2fffd255499 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 Jun 2023 21:28:53 +1000 Subject: [PATCH 084/316] chore(deps): update dependency dotnet-sdk to v7.0.304 (#134) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 4b6743ee..7720a9f1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.302" + "version": "7.0.304" } } From 55c5e8e5c9e7667afb84d0b7946234e5274d4924 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 14 Jul 2023 14:52:25 -0400 Subject: [PATCH 085/316] fix: max System.Collections.Immutable version ++ (#137) Signed-off-by: Todd Baert --- build/Common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Common.props b/build/Common.props index 4c9a3d69..464ac37e 100644 --- a/build/Common.props +++ b/build/Common.props @@ -23,6 +23,6 @@ - + From 5ad83c4e5f37b533a7a0ad88126c4ccfbaa9f604 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 14:55:37 -0400 Subject: [PATCH 086/316] chore(main): release 1.3.0 (#133) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 12 ++++++++++++ build/Common.prod.props | 2 +- version.txt | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0ab6645..2a8f4ffd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.3.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed0d87e..3f913522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.3.0](https://github.com/open-feature/dotnet-sdk/compare/v1.2.0...v1.3.0) (2023-07-14) + + +### Features + +* Support for name client to given provider ([#129](https://github.com/open-feature/dotnet-sdk/issues/129)) ([3f765c6](https://github.com/open-feature/dotnet-sdk/commit/3f765c6fb4ccd651de2d4f46e1fec38cd26610fe)) + + +### Bug Fixes + +* max System.Collections.Immutable version ++ ([#137](https://github.com/open-feature/dotnet-sdk/issues/137)) ([55c5e8e](https://github.com/open-feature/dotnet-sdk/commit/55c5e8e5c9e7667afb84d0b7946234e5274d4924)) + ## [1.2.0](https://github.com/open-feature/dotnet-sdk/compare/v1.1.0...v1.2.0) (2023-02-14) diff --git a/build/Common.prod.props b/build/Common.prod.props index 4032dcd5..09ebbc76 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 1.2.0 + 1.3.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 26aaba0e..f0bb29e7 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.2.0 +1.3.0 From 15473b6c3ab969ca660b7f3a98e1999373517b42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 16:36:55 -0400 Subject: [PATCH 087/316] chore(deps): update dependency dotnet-sdk to v7.0.306 (#135) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 7720a9f1..eff948e4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.304" + "version": "7.0.306" } } From ecc970701ff46815d0116417232f7c6ea670bdef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:40:44 +1000 Subject: [PATCH 088/316] chore(deps): update dependency dotnet-sdk to v7.0.400 (#139) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .editorconfig | 3 ++- global.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index f67281eb..19afa364 100644 --- a/.editorconfig +++ b/.editorconfig @@ -140,7 +140,8 @@ dotnet_diagnostic.IDE0001.severity = warning dotnet_diagnostic.IDE0002.severity = warning # IDE0005: Remove unnecessary import -dotnet_diagnostic.IDE0005.severity = warning +# Workaround for https://github.com/dotnet/roslyn/issues/41640 +dotnet_diagnostic.IDE0005.severity = none # RS0041: Public members should not use oblivious types dotnet_diagnostic.RS0041.severity = suggestion diff --git a/global.json b/global.json index eff948e4..aeb8ae7b 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.306" + "version": "7.0.400" } } From 311987f0ed803b83f7cc1791ccc91129a3234a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:53:48 +0100 Subject: [PATCH 089/316] test: Remove moq library (#141) 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> --- build/Common.tests.props | 2 +- .../OpenFeature.E2ETests.csproj | 1 - .../OpenFeature.Tests/FeatureProviderTests.cs | 51 +- .../OpenFeature.Tests.csproj | 3 +- .../OpenFeatureClientTests.cs | 162 +++---- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 441 ++++++++---------- test/OpenFeature.Tests/OpenFeatureTests.cs | 10 +- 7 files changed, 279 insertions(+), 391 deletions(-) diff --git a/build/Common.tests.props b/build/Common.tests.props index 675f4e50..7ed10e63 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -25,7 +25,7 @@ [3.1.2] [6.7.0] [17.2.0] - [4.18.1] + [5.0.0] [2.4.3,3.0) [2.4.1,3.0) diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 5368701e..69ee2770 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -23,7 +23,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index ba159a97..9a355bbb 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using AutoFixture; using FluentAssertions; -using Moq; +using NSubstitute; using OpenFeature.Constant; using OpenFeature.Model; using OpenFeature.Tests.Internal; @@ -79,67 +79,62 @@ public async Task Provider_Must_ErrorType() var defaultIntegerValue = fixture.Create(); var defaultDoubleValue = fixture.Create(); var defaultStructureValue = fixture.Create(); - var providerMock = new Mock(MockBehavior.Strict); + var providerMock = Substitute.For(); const string testMessage = "An error message"; - providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, + providerMock.ResolveBooleanValue(flagName, defaultBoolValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.Setup(x => x.ResolveIntegerValue(flagName, defaultIntegerValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, + providerMock.ResolveIntegerValue(flagName, defaultIntegerValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.Setup(x => x.ResolveDoubleValue(flagName, defaultDoubleValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.InvalidContext, + providerMock.ResolveDoubleValue(flagName, defaultDoubleValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.InvalidContext, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.Setup(x => x.ResolveStringValue(flagName, defaultStringValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, + providerMock.ResolveStringValue(flagName, defaultStringValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.Setup(x => - x.ResolveStructureValue(flagName, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, + providerMock.ResolveStructureValue(flagName, defaultStructureValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.Setup(x => - x.ResolveStructureValue(flagName2, defaultStructureValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName2, defaultStructureValue, ErrorType.ProviderNotReady, + providerMock.ResolveStructureValue(flagName2, defaultStructureValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName2, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.Setup(x => x.ResolveBooleanValue(flagName2, defaultBoolValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName2, defaultBoolValue, ErrorType.TargetingKeyMissing, + providerMock.ResolveBooleanValue(flagName2, defaultBoolValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName2, defaultBoolValue, ErrorType.TargetingKeyMissing, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - - var provider = providerMock.Object; - - var boolRes = await provider.ResolveBooleanValue(flagName, defaultBoolValue); + var boolRes = await providerMock.ResolveBooleanValue(flagName, defaultBoolValue); boolRes.ErrorType.Should().Be(ErrorType.General); boolRes.ErrorMessage.Should().Be(testMessage); - var intRes = await provider.ResolveIntegerValue(flagName, defaultIntegerValue); + var intRes = await providerMock.ResolveIntegerValue(flagName, defaultIntegerValue); intRes.ErrorType.Should().Be(ErrorType.ParseError); intRes.ErrorMessage.Should().Be(testMessage); - var doubleRes = await provider.ResolveDoubleValue(flagName, defaultDoubleValue); + var doubleRes = await providerMock.ResolveDoubleValue(flagName, defaultDoubleValue); doubleRes.ErrorType.Should().Be(ErrorType.InvalidContext); doubleRes.ErrorMessage.Should().Be(testMessage); - var stringRes = await provider.ResolveStringValue(flagName, defaultStringValue); + var stringRes = await providerMock.ResolveStringValue(flagName, defaultStringValue); stringRes.ErrorType.Should().Be(ErrorType.TypeMismatch); stringRes.ErrorMessage.Should().Be(testMessage); - var structRes1 = await provider.ResolveStructureValue(flagName, defaultStructureValue); + var structRes1 = await providerMock.ResolveStructureValue(flagName, defaultStructureValue); structRes1.ErrorType.Should().Be(ErrorType.FlagNotFound); structRes1.ErrorMessage.Should().Be(testMessage); - var structRes2 = await provider.ResolveStructureValue(flagName2, defaultStructureValue); + var structRes2 = await providerMock.ResolveStructureValue(flagName2, defaultStructureValue); structRes2.ErrorType.Should().Be(ErrorType.ProviderNotReady); structRes2.ErrorMessage.Should().Be(testMessage); - var boolRes2 = await provider.ResolveBooleanValue(flagName2, defaultBoolValue); + var boolRes2 = await providerMock.ResolveBooleanValue(flagName2, defaultBoolValue); boolRes2.ErrorType.Should().Be(ErrorType.TargetingKeyMissing); boolRes2.ErrorMessage.Should().BeNull(); } diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 7f630ef0..6c719638 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -18,7 +18,8 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index aec5a631..fef7403a 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -5,7 +5,9 @@ using AutoFixture; using FluentAssertions; using Microsoft.Extensions.Logging; -using Moq; +using Microsoft.Extensions.Logging.Internal; +using NSubstitute; +using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; using OpenFeature.Error; using OpenFeature.Model; @@ -22,9 +24,9 @@ public void OpenFeatureClient_Should_Allow_Hooks() { var fixture = new Fixture(); var clientName = fixture.Create(); - var hook1 = new Mock(MockBehavior.Strict).Object; - var hook2 = new Mock(MockBehavior.Strict).Object; - var hook3 = new Mock(MockBehavior.Strict).Object; + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + var hook3 = Substitute.For(); var client = Api.Instance.GetClient(clientName); @@ -160,35 +162,28 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var mockedFeatureProvider = new Mock(MockBehavior.Strict); - var mockedLogger = new Mock>(MockBehavior.Default); + var mockedFeatureProvider = Substitute.For(); + var mockedLogger = Substitute.For>(); // This will fail to case a String to TestStructure - mockedFeatureProvider - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .Throws(); - mockedFeatureProvider.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - mockedFeatureProvider.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + mockedFeatureProvider.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Throws(); + mockedFeatureProvider.GetMetadata().Returns(new Metadata(fixture.Create())); + mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(mockedFeatureProvider.Object); - var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger.Object); + Api.Instance.SetProvider(mockedFeatureProvider); + var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger); var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); - mockedFeatureProvider - .Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); - - mockedLogger.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((o, t) => string.Equals($"Error while evaluating flag {flagName}", o.ToString(), StringComparison.InvariantCultureIgnoreCase)), - It.IsAny(), - It.IsAny>()), - Times.Once); + _ = mockedFeatureProvider.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + + mockedLogger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(t => string.Equals($"Error while evaluating flag {flagName}", t.ToString(), StringComparison.InvariantCultureIgnoreCase)), + Arg.Any(), + Arg.Any>()); } [Fact] @@ -200,21 +195,17 @@ public async Task Should_Resolve_BooleanValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(MockBehavior.Strict); - featureProviderMock - .Setup(x => x.ResolveBooleanValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveBooleanValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock.Object); + Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveBooleanValue(flagName, defaultValue, It.IsAny()), Times.Once); + _ = featureProviderMock.Received(1).ResolveBooleanValue(flagName, defaultValue, Arg.Any()); } [Fact] @@ -226,21 +217,17 @@ public async Task Should_Resolve_StringValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(MockBehavior.Strict); - featureProviderMock - .Setup(x => x.ResolveStringValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStringValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock.Object); + Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetStringValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveStringValue(flagName, defaultValue, It.IsAny()), Times.Once); + _ = featureProviderMock.Received(1).ResolveStringValue(flagName, defaultValue, Arg.Any()); } [Fact] @@ -252,21 +239,17 @@ public async Task Should_Resolve_IntegerValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(MockBehavior.Strict); - featureProviderMock - .Setup(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveIntegerValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock.Object); + Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveIntegerValue(flagName, defaultValue, It.IsAny()), Times.Once); + _ = featureProviderMock.Received(1).ResolveIntegerValue(flagName, defaultValue, Arg.Any()); } [Fact] @@ -278,21 +261,17 @@ public async Task Should_Resolve_DoubleValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(MockBehavior.Strict); - featureProviderMock - .Setup(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveDoubleValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock.Object); + Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveDoubleValue(flagName, defaultValue, It.IsAny()), Times.Once); + _ = featureProviderMock.Received(1).ResolveDoubleValue(flagName, defaultValue, Arg.Any()); } [Fact] @@ -304,21 +283,17 @@ public async Task Should_Resolve_StructureValue() var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var featureProviderMock = new Mock(MockBehavior.Strict); - featureProviderMock - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .ReturnsAsync(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock.Object); + Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); - featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); + _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); } [Fact] @@ -331,24 +306,19 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() var defaultValue = fixture.Create(); const string testMessage = "Couldn't parse flag data."; - var featureProviderMock = new Mock(MockBehavior.Strict); - featureProviderMock - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, - "ERROR", null, testMessage))); - featureProviderMock.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); - - Api.Instance.SetProvider(featureProviderMock.Object); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, "ERROR", null, testMessage))); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + + Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); response.Reason.Should().Be(Reason.Error); response.ErrorMessage.Should().Be(testMessage); - featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); + _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); } [Fact] @@ -361,23 +331,19 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var defaultValue = fixture.Create(); const string testMessage = "Couldn't parse flag data."; - var featureProviderMock = new Mock(MockBehavior.Strict); - featureProviderMock - .Setup(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny())) - .Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); - featureProviderMock.Setup(x => x.GetMetadata()) - .Returns(new Metadata(fixture.Create())); - featureProviderMock.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock.Object); + Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); response.Reason.Should().Be(Reason.Error); response.ErrorMessage.Should().Be(testMessage); - featureProviderMock.Verify(x => x.ResolveStructureValue(flagName, defaultValue, It.IsAny()), Times.Once); + _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index df127790..e81c5d3c 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -6,6 +6,8 @@ using AutoFixture; using FluentAssertions; using Moq; +using NSubstitute; +using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; using OpenFeature.Model; using OpenFeature.Tests.Internal; @@ -26,102 +28,63 @@ public async Task Hooks_Should_Be_Called_In_Order() var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); - var apiHook = new Mock(MockBehavior.Strict); - var clientHook = new Mock(MockBehavior.Strict); - var invocationHook = new Mock(MockBehavior.Strict); - var providerHook = new Mock(MockBehavior.Strict); - - var sequence = new MockSequence(); - - apiHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), - It.IsAny>())) - .ReturnsAsync(EvaluationContext.Empty); - - clientHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), - It.IsAny>())) - .ReturnsAsync(EvaluationContext.Empty); - - invocationHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), - It.IsAny>())) - .ReturnsAsync(EvaluationContext.Empty); - - providerHook.InSequence(sequence).Setup(x => x.Before(It.IsAny>(), - It.IsAny>())) - .ReturnsAsync(EvaluationContext.Empty); - - providerHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); - - invocationHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); - - clientHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); - - apiHook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); - - providerHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); - - invocationHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); - - clientHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); - - apiHook.InSequence(sequence).Setup(x => x.Finally(It.IsAny>(), - It.IsAny>())).Returns(Task.CompletedTask); + var apiHook = Substitute.For(); + var clientHook = Substitute.For(); + var invocationHook = Substitute.For(); + var providerHook = Substitute.For(); + + // Sequence + apiHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + clientHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + invocationHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + invocationHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + clientHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + apiHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + providerHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + invocationHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + clientHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + apiHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); var testProvider = new TestProvider(); - testProvider.AddHook(providerHook.Object); - Api.Instance.AddHooks(apiHook.Object); + testProvider.AddHook(providerHook); + Api.Instance.AddHooks(apiHook); Api.Instance.SetProvider(testProvider); var client = Api.Instance.GetClient(clientName, clientVersion); - client.AddHooks(clientHook.Object); + client.AddHooks(clientHook); await client.GetBooleanValue(flagName, defaultValue, EvaluationContext.Empty, - new FlagEvaluationOptions(invocationHook.Object, ImmutableDictionary.Empty)); - - apiHook.Verify(x => x.Before( - It.IsAny>(), It.IsAny>()), Times.Once); - - clientHook.Verify(x => x.Before( - It.IsAny>(), It.IsAny>()), Times.Once); - - invocationHook.Verify(x => x.Before( - It.IsAny>(), It.IsAny>()), Times.Once); - - providerHook.Verify(x => x.Before( - It.IsAny>(), It.IsAny>()), Times.Once); - - providerHook.Verify(x => x.After( - It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); - - invocationHook.Verify(x => x.After( - It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); - - clientHook.Verify(x => x.After( - It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); - - apiHook.Verify(x => x.After( - It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); - - providerHook.Verify(x => x.Finally( - It.IsAny>(), It.IsAny>()), Times.Once); - - invocationHook.Verify(x => x.Finally( - It.IsAny>(), It.IsAny>()), Times.Once); - - clientHook.Verify(x => x.Finally( - It.IsAny>(), It.IsAny>()), Times.Once); + new FlagEvaluationOptions(invocationHook, ImmutableDictionary.Empty)); - apiHook.Verify(x => x.Finally( - It.IsAny>(), It.IsAny>()), Times.Once); + Received.InOrder(() => + { + apiHook.Before(Arg.Any>(), Arg.Any>()); + clientHook.Before(Arg.Any>(), Arg.Any>()); + invocationHook.Before(Arg.Any>(), Arg.Any>()); + providerHook.Before(Arg.Any>(), Arg.Any>()); + providerHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + invocationHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + clientHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + apiHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + providerHook.Finally(Arg.Any>(), Arg.Any>()); + invocationHook.Finally(Arg.Any>(), Arg.Any>()); + clientHook.Finally(Arg.Any>(), Arg.Any>()); + apiHook.Finally(Arg.Any>(), Arg.Any>()); + }); + + _ = apiHook.Received(1).Before(Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).Before(Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).Before(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).Before(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); } [Fact] @@ -169,25 +132,21 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() { var evaluationContext = new EvaluationContextBuilder().Set("test", "test").Build(); - var hook1 = new Mock(MockBehavior.Strict); - var hook2 = new Mock(MockBehavior.Strict); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); var hookContext = new HookContext("test", false, FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName), evaluationContext); - hook1.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(evaluationContext); - - hook2.Setup(x => - x.Before(hookContext, It.IsAny>())) - .ReturnsAsync(evaluationContext); + hook1.Before(Arg.Any>(), Arg.Any>()).Returns(evaluationContext); + hook2.Before(hookContext, Arg.Any>()).Returns(evaluationContext); var client = Api.Instance.GetClient("test", "1.0.0"); await client.GetBooleanValue("test", false, EvaluationContext.Empty, - new FlagEvaluationOptions(ImmutableList.Create(hook1.Object, hook2.Object), ImmutableDictionary.Empty)); + new FlagEvaluationOptions(ImmutableList.Create(hook1, hook2), ImmutableDictionary.Empty)); - hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.Before(It.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), It.IsAny>()), Times.Once); + _ = hook1.Received(1).Before(Arg.Any>(), Arg.Any>()); + _ = hook2.Received(1).Before(Arg.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), Arg.Any>()); } [Fact] @@ -229,29 +188,25 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() .Set(propInvocationToOverwrite, true) .Build(); - var provider = new Mock(MockBehavior.Strict); + var provider = Substitute.For(); - provider.Setup(x => x.GetMetadata()) - .Returns(new Metadata(null)); + provider.GetMetadata().Returns(new Metadata(null)); - provider.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + provider.GetProviderHooks().Returns(ImmutableList.Empty); - provider.Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ResolutionDetails("test", true)); + provider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); - Api.Instance.SetProvider(provider.Object); + Api.Instance.SetProvider(provider); - var hook = new Mock(MockBehavior.Strict); - hook.Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(hookContext); + var hook = Substitute.For(); + hook.Before(Arg.Any>(), Arg.Any>()).Returns(hookContext); var client = Api.Instance.GetClient("test", "1.0.0", null, clientContext); - await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook.Object), ImmutableDictionary.Empty)); + await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty)); // after proper merging, all properties should equal true - provider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.Is(y => + _ = provider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Is(y => (y.GetValue(propGlobal).AsBoolean ?? false) && (y.GetValue(propClient).AsBoolean ?? false) && (y.GetValue(propGlobalToOverwrite).AsBoolean ?? false) @@ -259,7 +214,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() && (y.GetValue(propClientToOverwrite).AsBoolean ?? false) && (y.GetValue(propHook).AsBoolean ?? false) && (y.GetValue(propInvocationToOverwrite).AsBoolean ?? false) - )), Times.Once); + )); } [Fact] @@ -301,60 +256,55 @@ public async Task Hook_Should_Return_No_Errors() [Specification("4.5.3", "The hook MUST NOT alter the `hook hints` structure.")] public async Task Hook_Should_Execute_In_Correct_Order() { - var featureProvider = new Mock(MockBehavior.Strict); - var hook = new Mock(MockBehavior.Strict); + var featureProvider = Substitute.For(); + var hook = Substitute.For(); - var sequence = new MockSequence(); + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - featureProvider.Setup(x => x.GetMetadata()) - .Returns(new Metadata(null)); + // Sequence + hook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + _ = hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.Finally(Arg.Any>(), Arg.Any>()); - featureProvider.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); - - hook.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), It.IsAny>())) - .ReturnsAsync(EvaluationContext.Empty); - - featureProvider.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ResolutionDetails("test", false)); - - hook.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), It.IsAny>())); - - hook.InSequence(sequence).Setup(x => - x.Finally(It.IsAny>(), It.IsAny>())); - - Api.Instance.SetProvider(featureProvider.Object); + Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); - client.AddHooks(hook.Object); + client.AddHooks(hook); await client.GetBooleanValue("test", false); - hook.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), It.IsAny>()), Times.Once); - hook.Verify(x => x.Finally(It.IsAny>(), It.IsAny>()), Times.Once); - featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + Received.InOrder(() => + { + hook.Before(Arg.Any>(), Arg.Any>()); + featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); + hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Finally(Arg.Any>(), Arg.Any>()); + }); + + _ = hook.Received(1).Before(Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + _ = featureProvider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] [Specification("4.4.1", "The API, Client, Provider, and invocation MUST have a method for registering hooks.")] public async Task Register_Hooks_Should_Be_Available_At_All_Levels() { - var hook1 = new Mock(MockBehavior.Strict); - var hook2 = new Mock(MockBehavior.Strict); - var hook3 = new Mock(MockBehavior.Strict); - var hook4 = new Mock(MockBehavior.Strict); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + var hook3 = Substitute.For(); + var hook4 = Substitute.For(); var testProvider = new TestProvider(); - testProvider.AddHook(hook4.Object); - Api.Instance.AddHooks(hook1.Object); + testProvider.AddHook(hook4); + Api.Instance.AddHooks(hook1); Api.Instance.SetProvider(testProvider); var client = Api.Instance.GetClient(); - client.AddHooks(hook2.Object); + client.AddHooks(hook2); await client.GetBooleanValue("test", false, null, - new FlagEvaluationOptions(hook3.Object, ImmutableDictionary.Empty)); + new FlagEvaluationOptions(hook3, ImmutableDictionary.Empty)); Assert.Single(Api.Instance.GetHooks()); client.GetHooks().Count().Should().Be(1); @@ -365,144 +315,121 @@ await client.GetBooleanValue("test", false, null, [Specification("4.4.3", "If a `finally` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `finally` hooks.")] public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() { - var featureProvider = new Mock(MockBehavior.Strict); - var hook1 = new Mock(MockBehavior.Strict); - var hook2 = new Mock(MockBehavior.Strict); - - var sequence = new MockSequence(); - - featureProvider.Setup(x => x.GetMetadata()) - .Returns(new Metadata(null)); - - featureProvider.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); - - hook1.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), null)) - .ReturnsAsync(EvaluationContext.Empty); - - hook2.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), null)) - .ReturnsAsync(EvaluationContext.Empty); - - featureProvider.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ResolutionDetails("test", false)); - - hook2.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), null)) - .Returns(Task.CompletedTask); - - hook1.InSequence(sequence).Setup(x => x.After(It.IsAny>(), - It.IsAny>(), null)) - .Returns(Task.CompletedTask); - - hook2.InSequence(sequence).Setup(x => - x.Finally(It.IsAny>(), null)) - .Returns(Task.CompletedTask); - - hook1.InSequence(sequence).Setup(x => - x.Finally(It.IsAny>(), null)) - .Throws(new Exception()); - - Api.Instance.SetProvider(featureProvider.Object); + var featureProvider = Substitute.For(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + + // Sequence + hook1.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); + hook2.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + hook2.After(Arg.Any>(), Arg.Any>(), null).Returns(Task.CompletedTask); + hook1.After(Arg.Any>(), Arg.Any>(), null).Returns(Task.CompletedTask); + hook2.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); + hook1.Finally(Arg.Any>(), null).Throws(new Exception()); + + Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); - client.AddHooks(new[] { hook1.Object, hook2.Object }); + client.AddHooks(new[] { hook1, hook2 }); client.GetHooks().Count().Should().Be(2); await client.GetBooleanValue("test", false); - hook1.Verify(x => x.Before(It.IsAny>(), null), Times.Once); - hook2.Verify(x => x.Before(It.IsAny>(), null), Times.Once); - featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - hook2.Verify(x => x.After(It.IsAny>(), It.IsAny>(), null), Times.Once); - hook1.Verify(x => x.After(It.IsAny>(), It.IsAny>(), null), Times.Once); - hook2.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); - hook1.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); + Received.InOrder(() => + { + hook1.Before(Arg.Any>(), null); + hook2.Before(Arg.Any>(), null); + featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.After(Arg.Any>(), Arg.Any>(), null); + hook1.After(Arg.Any>(), Arg.Any>(), null); + hook2.Finally(Arg.Any>(), null); + hook1.Finally(Arg.Any>(), null); + }); + + _ = hook1.Received(1).Before(Arg.Any>(), null); + _ = hook2.Received(1).Before(Arg.Any>(), null); + _ = featureProvider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); + _ = hook2.Received(1).After(Arg.Any>(), Arg.Any>(), null); + _ = hook1.Received(1).After(Arg.Any>(), Arg.Any>(), null); + _ = hook2.Received(1).Finally(Arg.Any>(), null); + _ = hook1.Received(1).Finally(Arg.Any>(), null); } [Fact] [Specification("4.4.4", "If an `error` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `error` hooks.")] public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() { - var featureProvider1 = new Mock(MockBehavior.Strict); - var hook1 = new Mock(MockBehavior.Strict); - var hook2 = new Mock(MockBehavior.Strict); - - var sequence = new MockSequence(); - - featureProvider1.Setup(x => x.GetMetadata()) - .Returns(new Metadata(null)); - featureProvider1.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProvider1 = Substitute.For(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); - hook1.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), null)) - .ReturnsAsync(EvaluationContext.Empty); + featureProvider1.GetMetadata().Returns(new Metadata(null)); + featureProvider1.GetProviderHooks().Returns(ImmutableList.Empty); - hook2.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), null)) - .ReturnsAsync(EvaluationContext.Empty); - - featureProvider1.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new Exception()); - - hook2.InSequence(sequence).Setup(x => - x.Error(It.IsAny>(), It.IsAny(), null)) - .Returns(Task.CompletedTask); - - hook1.InSequence(sequence).Setup(x => - x.Error(It.IsAny>(), It.IsAny(), null)) - .Returns(Task.CompletedTask); + // Sequence + hook1.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); + hook2.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); + featureProvider1.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Throws(new Exception()); + hook2.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); + hook1.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); - Api.Instance.SetProvider(featureProvider1.Object); + Api.Instance.SetProvider(featureProvider1); var client = Api.Instance.GetClient(); - client.AddHooks(new[] { hook1.Object, hook2.Object }); + client.AddHooks(new[] { hook1, hook2 }); await client.GetBooleanValue("test", false); - hook1.Verify(x => x.Before(It.IsAny>(), null), Times.Once); - hook2.Verify(x => x.Before(It.IsAny>(), null), Times.Once); - hook1.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); - hook2.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); + Received.InOrder(() => + { + hook1.Before(Arg.Any>(), null); + hook2.Before(Arg.Any>(), null); + featureProvider1.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.Error(Arg.Any>(), Arg.Any(), null); + hook1.Error(Arg.Any>(), Arg.Any(), null); + }); + + _ = hook1.Received(1).Before(Arg.Any>(), null); + _ = hook2.Received(1).Before(Arg.Any>(), null); + _ = hook1.Received(1).Error(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).Error(Arg.Any>(), Arg.Any(), null); } [Fact] [Specification("4.4.6", "If an error occurs during the evaluation of `before` or `after` hooks, any remaining hooks in the `before` or `after` stages MUST NOT be invoked.")] public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_Any_Remaining_Hooks() { - var featureProvider = new Mock(MockBehavior.Strict); - var hook1 = new Mock(MockBehavior.Strict); - var hook2 = new Mock(MockBehavior.Strict); - - var sequence = new MockSequence(); - - featureProvider.Setup(x => x.GetMetadata()) - .Returns(new Metadata(null)); - featureProvider.Setup(x => x.GetProviderHooks()) - .Returns(ImmutableList.Empty); + var featureProvider = Substitute.For(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); - hook1.InSequence(sequence).Setup(x => - x.Before(It.IsAny>(), It.IsAny>())) - .ThrowsAsync(new Exception()); + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - hook1.InSequence(sequence).Setup(x => - x.Error(It.IsAny>(), It.IsAny(), null)); + // Sequence + hook1.Before(Arg.Any>(), Arg.Any>()).ThrowsAsync(new Exception()); + _ = hook1.Error(Arg.Any>(), Arg.Any(), null); + _ = hook2.Error(Arg.Any>(), Arg.Any(), null); - hook2.InSequence(sequence).Setup(x => - x.Error(It.IsAny>(), It.IsAny(), null)); - - Api.Instance.SetProvider(featureProvider.Object); + Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); - client.AddHooks(new[] { hook1.Object, hook2.Object }); + client.AddHooks(new[] { hook1, hook2 }); await client.GetBooleanValue("test", false); - hook1.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Once); - hook2.Verify(x => x.Before(It.IsAny>(), It.IsAny>()), Times.Never); - hook1.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); - hook2.Verify(x => x.Error(It.IsAny>(), It.IsAny(), null), Times.Once); + Received.InOrder(() => + { + hook1.Before(Arg.Any>(), Arg.Any>()); + hook2.Error(Arg.Any>(), Arg.Any(), null); + hook1.Error(Arg.Any>(), Arg.Any(), null); + }); + + _ = hook1.Received(1).Before(Arg.Any>(), Arg.Any>()); + _ = hook2.DidNotReceive().Before(Arg.Any>(), Arg.Any>()); + _ = hook1.Received(1).Error(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).Error(Arg.Any>(), Arg.Any(), null); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index f6ad03a0..9776d552 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,6 +1,6 @@ using System.Linq; using FluentAssertions; -using Moq; +using NSubstitute; using OpenFeature.Constant; using OpenFeature.Model; using OpenFeature.Tests.Internal; @@ -83,10 +83,10 @@ public void OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance() public void OpenFeature_Should_Add_Hooks() { var openFeature = Api.Instance; - var hook1 = new Mock(MockBehavior.Strict).Object; - var hook2 = new Mock(MockBehavior.Strict).Object; - var hook3 = new Mock(MockBehavior.Strict).Object; - var hook4 = new Mock(MockBehavior.Strict).Object; + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + var hook3 = Substitute.For(); + var hook4 = Substitute.For(); openFeature.ClearHooks(); From e9858d9d66ad75ff13f6becc311c50d8dd75caa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:57:18 +0100 Subject: [PATCH 090/316] test: Replace Moq with NSubstitute (#142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index e81c5d3c..aa0c75c4 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -482,34 +482,33 @@ public async Task Hook_Hints_May_Be_Optional() [Specification("4.4.7", "If an error occurs in the `before` hooks, the default value MUST be returned.")] public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() { - var featureProvider = new Mock(MockBehavior.Strict); - var hook = new Mock(MockBehavior.Strict); + var featureProvider = Substitute.For(); + var hook = Substitute.For(); var exceptionToThrow = new Exception("Fails during default"); - var sequence = new MockSequence(); - - featureProvider.Setup(x => x.GetMetadata()) - .Returns(new Metadata(null)); - - hook.InSequence(sequence) - .Setup(x => x.Before(It.IsAny>(), It.IsAny>())) - .ThrowsAsync(exceptionToThrow); - - hook.InSequence(sequence) - .Setup(x => x.Error(It.IsAny>(), It.IsAny(), null)).Returns(Task.CompletedTask); + featureProvider.GetMetadata().Returns(new Metadata(null)); - hook.InSequence(sequence) - .Setup(x => x.Finally(It.IsAny>(), null)).Returns(Task.CompletedTask); + // Sequence + hook.Before(Arg.Any>(), Arg.Any>()).ThrowsAsync(exceptionToThrow); + hook.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); + hook.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); var client = Api.Instance.GetClient(); - client.AddHooks(hook.Object); + client.AddHooks(hook); var resolvedFlag = await client.GetBooleanValue("test", true); + Received.InOrder(() => + { + hook.Before(Arg.Any>(), Arg.Any>()); + hook.Error(Arg.Any>(), Arg.Any(), null); + hook.Finally(Arg.Any>(), null); + }); + resolvedFlag.Should().BeTrue(); - hook.Verify(x => x.Before(It.IsAny>(), null), Times.Once); - hook.Verify(x => x.Error(It.IsAny>(), exceptionToThrow, null), Times.Once); - hook.Verify(x => x.Finally(It.IsAny>(), null), Times.Once); + _ = hook.Received(1).Before(Arg.Any>(), null); + _ = hook.Received(1).Error(Arg.Any>(), exceptionToThrow, null); + _ = hook.Received(1).Finally(Arg.Any>(), null); } [Fact] From 598270d3f19702cab84c0f4ac4b1d63459e8e0fc Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Tue, 22 Aug 2023 01:45:48 +1000 Subject: [PATCH 091/316] test: Replace remaining Moq with NSubstitute (#143) Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .../OpenFeature.Tests.csproj | 1 - .../OpenFeature.Tests/OpenFeatureHookTests.cs | 97 ++++++++----------- 2 files changed, 43 insertions(+), 55 deletions(-) diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 6c719638..d1b77419 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -18,7 +18,6 @@ - diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index aa0c75c4..c814b15a 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using AutoFixture; using FluentAssertions; -using Moq; using NSubstitute; using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; @@ -436,45 +435,40 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ [Specification("4.5.1", "`Flag evaluation options` MAY contain `hook hints`, a map of data to be provided to hook invocations.")] public async Task Hook_Hints_May_Be_Optional() { - var featureProvider = new Mock(MockBehavior.Strict); - var hook = new Mock(MockBehavior.Strict); - var defaultEmptyHookHints = new Dictionary(); - var flagOptions = new FlagEvaluationOptions(hook.Object); - EvaluationContext evaluationContext = null; - - var sequence = new MockSequence(); + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var flagOptions = new FlagEvaluationOptions(hook); - featureProvider.Setup(x => x.GetMetadata()) + featureProvider.GetMetadata() .Returns(new Metadata(null)); - featureProvider.Setup(x => x.GetProviderHooks()) + featureProvider.GetProviderHooks() .Returns(ImmutableList.Empty); - hook.InSequence(sequence) - .Setup(x => x.Before(It.IsAny>(), defaultEmptyHookHints)) - .ReturnsAsync(evaluationContext); + hook.Before(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty); - featureProvider.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ResolutionDetails("test", false)); + featureProvider.ResolveBooleanValue("test", false, Arg.Any()) + .Returns(new ResolutionDetails("test", false)); - hook.InSequence(sequence) - .Setup(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints)) - .Returns(Task.CompletedTask); + hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(Task.FromResult(Task.CompletedTask)); - hook.InSequence(sequence) - .Setup(x => x.Finally(It.IsAny>(), defaultEmptyHookHints)) + hook.Finally(Arg.Any>(), Arg.Any>()) .Returns(Task.CompletedTask); - Api.Instance.SetProvider(featureProvider.Object); + Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); - await client.GetBooleanValue("test", false, config: flagOptions); + await client.GetBooleanValue("test", false, EvaluationContext.Empty, flagOptions); - hook.Verify(x => x.Before(It.IsAny>(), defaultEmptyHookHints), Times.Once); - hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints), Times.Once); - hook.Verify(x => x.Finally(It.IsAny>(), defaultEmptyHookHints), Times.Once); - featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + Received.InOrder(() => + { + hook.Received().Before(Arg.Any>(), Arg.Any>()); + featureProvider.Received().ResolveBooleanValue("test", false, Arg.Any()); + hook.Received().After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received().Finally(Arg.Any>(), Arg.Any>()); + }); } [Fact] @@ -515,52 +509,47 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() [Specification("4.4.5", "If an error occurs in the `before` or `after` hooks, the `error` hooks MUST be invoked.")] public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() { - var featureProvider = new Mock(MockBehavior.Strict); - var hook = new Mock(MockBehavior.Strict); - var defaultEmptyHookHints = new Dictionary(); - var flagOptions = new FlagEvaluationOptions(hook.Object); + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var flagOptions = new FlagEvaluationOptions(hook); var exceptionToThrow = new Exception("Fails during default"); - EvaluationContext evaluationContext = null; - - var sequence = new MockSequence(); - featureProvider.Setup(x => x.GetMetadata()) + featureProvider.GetMetadata() .Returns(new Metadata(null)); - featureProvider.Setup(x => x.GetProviderHooks()) + featureProvider.GetProviderHooks() .Returns(ImmutableList.Empty); - hook.InSequence(sequence) - .Setup(x => x.Before(It.IsAny>(), defaultEmptyHookHints)) - .ReturnsAsync(evaluationContext); + hook.Before(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty); - featureProvider.InSequence(sequence) - .Setup(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ResolutionDetails("test", false)); + featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new ResolutionDetails("test", false)); - hook.InSequence(sequence) - .Setup(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints)) + hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()) .ThrowsAsync(exceptionToThrow); - hook.InSequence(sequence) - .Setup(x => x.Error(It.IsAny>(), It.IsAny(), defaultEmptyHookHints)) + hook.Error(Arg.Any>(), Arg.Any(), Arg.Any>()) .Returns(Task.CompletedTask); - hook.InSequence(sequence) - .Setup(x => x.Finally(It.IsAny>(), defaultEmptyHookHints)) + hook.Finally(Arg.Any>(), Arg.Any>()) .Returns(Task.CompletedTask); - Api.Instance.SetProvider(featureProvider.Object); + Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); var resolvedFlag = await client.GetBooleanValue("test", true, config: flagOptions); resolvedFlag.Should().BeTrue(); - hook.Verify(x => x.Before(It.IsAny>(), defaultEmptyHookHints), Times.Once); - hook.Verify(x => x.After(It.IsAny>(), It.IsAny>(), defaultEmptyHookHints), Times.Once); - hook.Verify(x => x.Error(It.IsAny>(), exceptionToThrow, defaultEmptyHookHints), Times.Once); - hook.Verify(x => x.Finally(It.IsAny>(), defaultEmptyHookHints), Times.Once); - featureProvider.Verify(x => x.ResolveBooleanValue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + Received.InOrder(() => + { + hook.Received(1).Before(Arg.Any>(), Arg.Any>()); + hook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + }); + + await featureProvider.DidNotReceive().ResolveBooleanValue("test", false, Arg.Any()); } } } From 3da02e67a6e1e11af72fbd38aa42215b41b4e33b Mon Sep 17 00:00:00 2001 From: David Hirsch Date: Mon, 11 Sep 2023 18:32:17 +0200 Subject: [PATCH 092/316] docs: Update README.md (#147) Signed-off-by: David Hirsch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08a38f2a..7fa57c19 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ### What is OpenFeature? -[OpenFeature][openfeature-website] is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. +[OpenFeature][openfeature-website] is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. ### Why standardize feature flags? From 17a7772c0dad9c68a4a0e0e272fe32ce3bfe0cff Mon Sep 17 00:00:00 2001 From: DouglasHammon-FV <132104255+DouglasHammon-FV@users.noreply.github.com> Date: Tue, 19 Sep 2023 10:50:21 -0600 Subject: [PATCH 093/316] fix: deadlocks in client applications (#150) Signed-off-by: DouglasHammon-FV <132104255+DouglasHammon-FV@users.noreply.github.com> --- src/OpenFeature/OpenFeatureClient.cs | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 38a0fd63..1cc26802 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -107,63 +107,63 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - (await this.GetBooleanDetails(flagKey, defaultValue, context, config)).Value; + (await this.GetBooleanDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue), FlagValueType.Boolean, flagKey, - defaultValue, context, config); + defaultValue, context, config).ConfigureAwait(false); /// public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - (await this.GetStringDetails(flagKey, defaultValue, context, config)).Value; + (await this.GetStringDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue), FlagValueType.String, flagKey, - defaultValue, context, config); + defaultValue, context, config).ConfigureAwait(false); /// public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - (await this.GetIntegerDetails(flagKey, defaultValue, context, config)).Value; + (await this.GetIntegerDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue), FlagValueType.Number, flagKey, - defaultValue, context, config); + defaultValue, context, config).ConfigureAwait(false); /// public async Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - (await this.GetDoubleDetails(flagKey, defaultValue, context, config)).Value; + (await this.GetDoubleDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue), FlagValueType.Number, flagKey, - defaultValue, context, config); + defaultValue, context, config).ConfigureAwait(false); /// public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => - (await this.GetObjectDetails(flagKey, defaultValue, context, config)).Value; + (await this.GetObjectDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue), FlagValueType.Object, flagKey, - defaultValue, context, config); + defaultValue, context, config).ConfigureAwait(false); private async Task> EvaluateFlag( (Func>>, FeatureProvider) providerInfo, @@ -211,13 +211,13 @@ private async Task> EvaluateFlag( FlagEvaluationDetails evaluation; try { - var contextFromHooks = await this.TriggerBeforeHooks(allHooks, hookContext, options); + var contextFromHooks = await this.TriggerBeforeHooks(allHooks, hookContext, options).ConfigureAwait(false); evaluation = - (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext)) + (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext).ConfigureAwait(false)) .ToFlagEvaluationDetails(); - await this.TriggerAfterHooks(allHooksReversed, hookContext, evaluation, options); + await this.TriggerAfterHooks(allHooksReversed, hookContext, evaluation, options).ConfigureAwait(false); } catch (FeatureProviderException ex) { @@ -225,18 +225,18 @@ private async Task> EvaluateFlag( ex.ErrorType.GetDescription()); evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error, string.Empty, ex.Message); - await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options); + await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); } catch (Exception ex) { this._logger.LogError(ex, "Error while evaluating flag {FlagKey}", flagKey); var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General; evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty); - await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options); + await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); } finally { - await this.TriggerFinallyHooks(allHooksReversed, hookContext, options); + await this.TriggerFinallyHooks(allHooksReversed, hookContext, options).ConfigureAwait(false); } return evaluation; @@ -250,7 +250,7 @@ private async Task> TriggerBeforeHooks(IReadOnlyList hoo foreach (var hook in hooks) { - var resp = await hook.Before(context, options?.HookHints); + var resp = await hook.Before(context, options?.HookHints).ConfigureAwait(false); if (resp != null) { evalContextBuilder.Merge(resp); @@ -271,7 +271,7 @@ private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext { try { - await hook.Finally(context, options?.HookHints); + await hook.Finally(context, options?.HookHints).ConfigureAwait(false); } catch (Exception e) { From f921dc699a358070568be93027680d49e0f7cb8e Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 19 Sep 2023 12:56:10 -0400 Subject: [PATCH 094/316] chore: update rp config (emoji) Signed-off-by: Todd Baert --- release-please-config.json | 57 +++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/release-please-config.json b/release-please-config.json index 515dc1e4..3a34941e 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,5 +11,60 @@ "build/Common.prod.props" ] } - } + }, + "changelog-sections": [ + { + "type": "fix", + "section": "πŸ› Bug Fixes" + }, + { + "type": "feat", + "section": "✨ New Features" + }, + { + "type": "chore", + "section": "🧹 Chore" + }, + { + "type": "docs", + "section": "πŸ“š Documentation" + }, + { + "type": "perf", + "section": "πŸš€ Performance" + }, + { + "type": "build", + "hidden": true, + "section": "πŸ› οΈ Build" + }, + { + "type": "deps", + "section": "πŸ“¦ Dependencies" + }, + { + "type": "ci", + "hidden": true, + "section": "🚦 CI" + }, + { + "type": "refactor", + "section": "πŸ”„ Refactoring" + }, + { + "type": "revert", + "section": "πŸ”™ Reverts" + }, + { + "type": "style", + "hidden": true, + "section": "🎨 Styling" + }, + { + "type": "test", + "hidden": true, + "section": "πŸ§ͺ Tests" + } + ], + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" } From dd92cbf3ce379f32f66b3787e1cab6f14dd7de99 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:59:39 -0400 Subject: [PATCH 095/316] chore(main): release 1.3.1 (#153) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 19 +++++++++++++++++++ build/Common.prod.props | 2 +- version.txt | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2a8f4ffd..0e5b256d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.0" + ".": "1.3.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f913522..7eae4599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [1.3.1](https://github.com/open-feature/dotnet-sdk/compare/v1.3.0...v1.3.1) (2023-09-19) + + +### πŸ› Bug Fixes + +* deadlocks in client applications ([#150](https://github.com/open-feature/dotnet-sdk/issues/150)) ([17a7772](https://github.com/open-feature/dotnet-sdk/commit/17a7772c0dad9c68a4a0e0e272fe32ce3bfe0cff)) + + +### 🧹 Chore + +* **deps:** update dependency dotnet-sdk to v7.0.306 ([#135](https://github.com/open-feature/dotnet-sdk/issues/135)) ([15473b6](https://github.com/open-feature/dotnet-sdk/commit/15473b6c3ab969ca660b7f3a98e1999373517b42)) +* **deps:** update dependency dotnet-sdk to v7.0.400 ([#139](https://github.com/open-feature/dotnet-sdk/issues/139)) ([ecc9707](https://github.com/open-feature/dotnet-sdk/commit/ecc970701ff46815d0116417232f7c6ea670bdef)) +* update rp config (emoji) ([f921dc6](https://github.com/open-feature/dotnet-sdk/commit/f921dc699a358070568be93027680d49e0f7cb8e)) + + +### πŸ“š Documentation + +* Update README.md ([#147](https://github.com/open-feature/dotnet-sdk/issues/147)) ([3da02e6](https://github.com/open-feature/dotnet-sdk/commit/3da02e67a6e1e11af72fbd38aa42215b41b4e33b)) + ## [1.3.0](https://github.com/open-feature/dotnet-sdk/compare/v1.2.0...v1.3.0) (2023-07-14) diff --git a/build/Common.prod.props b/build/Common.prod.props index 09ebbc76..49e454c5 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,7 +6,7 @@ - 1.3.0 + 1.3.1 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index f0bb29e7..3a3cd8cc 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.3.0 +1.3.1 From 9c42d4afa9139094e0316bbe1306ae4856b7d013 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 22 Sep 2023 19:39:18 +1000 Subject: [PATCH 096/316] feat: Add dx to catch ConfigureAwait(false) (#152) Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .editorconfig | 3 +++ build/Common.props | 1 + test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs | 2 ++ test/OpenFeature.Tests/FeatureProviderTests.cs | 2 ++ test/OpenFeature.Tests/OpenFeatureClientTests.cs | 2 ++ test/OpenFeature.Tests/OpenFeatureHookTests.cs | 2 ++ 6 files changed, 12 insertions(+) diff --git a/.editorconfig b/.editorconfig index 19afa364..2682e4d6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -146,6 +146,9 @@ dotnet_diagnostic.IDE0005.severity = none # RS0041: Public members should not use oblivious types dotnet_diagnostic.RS0041.severity = suggestion +# CA2007: Do not directly await a Task +dotnet_diagnostic.CA2007.severity = error + [obj/**.cs] generated_code = true diff --git a/build/Common.props b/build/Common.props index 464ac37e..4af79a3c 100644 --- a/build/Common.props +++ b/build/Common.props @@ -2,6 +2,7 @@ 7.3 true + true diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index a17bfb8b..3adcb132 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using AutoFixture; using BenchmarkDotNet.Attributes; @@ -12,6 +13,7 @@ namespace OpenFeature.Benchmark [SimpleJob(RuntimeMoniker.Net60, baseline: true)] [JsonExporterAttribute.Full] [JsonExporterAttribute.FullCompressed] + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureClientBenchmarks { private readonly string _clientName; diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index 9a355bbb..aa79cbf9 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; @@ -9,6 +10,7 @@ namespace OpenFeature.Tests { + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index fef7403a..4995423e 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using AutoFixture; @@ -16,6 +17,7 @@ namespace OpenFeature.Tests { + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture { [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index c814b15a..72e4a1d0 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using AutoFixture; @@ -14,6 +15,7 @@ namespace OpenFeature.Tests { + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture { [Fact] From b62e21f76964e7f6f7456f720814de0997232d71 Mon Sep 17 00:00:00 2001 From: Darya <145902078+DaryaYuk@users.noreply.github.com> Date: Sat, 7 Oct 2023 00:36:03 +0300 Subject: [PATCH 097/316] docs: update README.md (#155) Signed-off-by: DaryaYuk --- README.md | 296 +++++++++++++++++++++++-------------- release-please-config.json | 3 +- 2 files changed, 187 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 7fa57c19..3693d785 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,59 @@ +

- - - OpenFeature Logo + + OpenFeature Logo

OpenFeature .NET SDK

-[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -[![spec version badge](https://img.shields.io/badge/Specification-v0.5.2-yellow)](https://github.com/open-feature/spec/tree/v0.5.2?rgh-link-date=2023-01-20T21%3A37%3A52Z) -[![codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) -[![nuget](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://bestpractices.coreinfrastructure.org/projects/6250) - -## πŸ‘‹ Hey there! Thanks for checking out the OpenFeature .NET SDK - -### What is OpenFeature? + + +

+ + Specification + + + + + Release + + + +
+ + Slack + + + Codecov + + + Codecov + + + CII Best Practices + + +

+ -[OpenFeature][openfeature-website] is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. +[OpenFeature](https://openfeature.dev) is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. -### Why standardize feature flags? + -Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition. +## πŸš€ Quick start -## πŸ” Requirements: +### Requirements -- .NET 6+ -- .NET Core 6+ -- .NET Framework 4.6.2+ +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 - -## πŸ“¦ Installation: +### Install Use the following to initialize your project: @@ -48,106 +67,94 @@ and install OpenFeature: dotnet add package OpenFeature ``` -## 🌟 Features: - -- support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider) -- easy integration and extension via [hooks](https://openfeature.dev/docs/reference/concepts/hooks) -- bool, string, numeric and object flag types -- [context-aware](https://openfeature.dev/docs/reference/concepts/evaluation-context) evaluation - -## πŸš€ Usage: - -### Basics: +### Usage ```csharp -using OpenFeature.Model; - -// Sets the provider used by the client -// If no provider is set, then a default NoOpProvider will be used. -//OpenFeature.Api.Instance.SetProvider(new MyProvider()); - -// Gets a instance of the feature flag client -var client = OpenFeature.Api.Instance.GetClient(); -// Evaluation the `my-feature` feature flag -var isEnabled = await client.GetBooleanValue("my-feature", false); +public async Task Example() + { + // Register your feature flag provider + Api.Instance.SetProvider(new InMemoryProvider()); + + // Create a new client + FeatureClient client = Api.Instance.GetClient(); + + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); + + if ( v2Enabled ) + { + //Do some work + } + } ``` -For complete documentation, visit: https://openfeature.dev/docs/category/concepts +## 🌟 Features -### Context-aware evaluation: - -Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server. -In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting). -If the flag system you're using supports targeting, you can provide the input data using the `EvaluationContext`. - -```csharp -using OpenFeature.Model; +| Status | Features | Description | +| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Logging](#logging) | Integrate with popular logging packages. | +| βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | +| ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ❌ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | -var client = OpenFeature.Api.Instance.GetClient(); +Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ -// Evaluating with a context. -var evaluationContext = EvaluationContext.Builder() - .Set("my-key", "my-value") - .Build(); +### Providers -// Evaluation the `my-conditional` feature flag -var isEnabled = await client.GetBooleanValue("my-conditional", false, evaluationContext); -``` +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). -### Providers: +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. Finally, you’ll then need to write the provider itself. This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: ```csharp -using OpenFeature; -using OpenFeature.Model; +Api.Instance.SetProvider(new MyProvider()); +``` -public class MyFeatureProvider : FeatureProvider -{ - public static string Name => "My Feature Provider"; +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [named clients](#named-clients), which is covered in more detail below. - public Metadata GetMetadata() - { - return new Metadata(Name); - } +### Targeting - public Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null) - { - // code to resolve boolean details - } +Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). - public Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null) - { - // code to resolve string details - } +```csharp +// set a value to the global context +EvaluationContextBuilder builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext apiCtx = builder.Build(); +Api.Instance.SetContext(apiCtx); + +// set a value to the client context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext clientCtx = builder.Build(); +var client = Api.Instance.GetClient(); +client.SetContext(clientCtx); - public Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null) - { - // code to resolve integer details - } +// set a value to the invocation context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext reqCtx = builder.Build(); - public Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null) - { - // code to resolve integer details - } +bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); - public Task> ResolveStructureValue(string flagKey, T defaultValue, - EvaluationContext context = null) - { - // code to resolve object details - } -} ``` -See [here](https://openfeature.dev/docs/reference/technologies/server/dotnet) for a catalog of available providers. +### Hooks -### Hooks: +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/). +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. -Hooks are a mechanism that allow for the addition of arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validation of the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking. +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. ```csharp // add a hook globally, to run on all evaluations @@ -161,7 +168,78 @@ client.AddHooks(new ExampleClientHook()); var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); ``` -Example of implementing a hook +### Logging + +The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for a complete documentation. + +### Named clients + +Clients can be given a name. +A name is a logical identifier that can be used to associate clients with a particular provider. +If a name has no associated provider, the global provider is used. + +```csharp +// registering the default provider +Api.Instance.SetProvider(new LocalProvider()); +// registering a named provider +Api.Instance.SetProvider("clientForCache", new CachedProvider()); + +// a client backed by default provider + FeatureClient clientDefault = Api.Instance.GetClient(); +// a client backed by CachedProvider +FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); + +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +```csharp +public class MyProvider : FeatureProvider + { + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + { + // resolve a boolean flag value + } + + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + { + // resolve a double flag value + } + + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + { + // resolve a string flag value + } + + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + { + // resolve an object flag value + } + } +``` + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. ```csharp public class MyHook : Hook @@ -191,26 +269,22 @@ public class MyHook : Hook } ``` -See [here](https://openfeature.dev/docs/reference/technologies/server/dotnet) for a catalog of available hooks. - -### Logging: - -The .NET SDK uses Microsoft Extensions Logger. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. +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! ## ⭐️ Support the project -- Give this repo a ⭐️! -- Follow us social media: - - Twitter: [@openfeature](https://twitter.com/openfeature) - - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) -- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more check out our [community page](https://openfeature.dev/community/) +- Give this repo a ⭐️! +- Follow us on social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more information, check out our [community page](https://openfeature.dev/community/) ## 🀝 Contributing Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. -### Thanks to everyone that has already contributed +### Thanks to everyone who has already contributed diff --git a/release-please-config.json b/release-please-config.json index 3a34941e..1c1e673c 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,7 +8,8 @@ "bump-patch-for-minor-pre-major": true, "versioning": "default", "extra-files": [ - "build/Common.prod.props" + "build/Common.prod.props", + "README.md" ] } }, From 6516866ec7601a7adaa4dc6b517c9287dec54fca Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Wed, 11 Oct 2023 15:16:07 -0400 Subject: [PATCH 098/316] chore: updated readme for inclusion in the docs Signed-off-by: Michael Beemer --- README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3693d785..de97348e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@

-[OpenFeature](https://openfeature.dev) is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. @@ -170,7 +170,7 @@ var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEva ### Logging -The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for a complete documentation. +The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. ### Named clients @@ -271,6 +271,7 @@ public class MyHook : Hook 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! + ## ⭐️ Support the project - Give this repo a ⭐️! @@ -291,9 +292,4 @@ Interested in contributing? Great, we'd love your help! To get started, take a l
Made with [contrib.rocks](https://contrib.rocks). - -## πŸ“œ License - -[Apache License 2.0](LICENSE) - -[openfeature-website]: https://openfeature.dev + From 9d8939ef57a3be4ee220bd21f36b166887b2c30b Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Wed, 11 Oct 2023 15:22:26 -0400 Subject: [PATCH 099/316] docs: fixed the contrib url on the readme Signed-off-by: Michael Beemer --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index de97348e..48c3af04 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); ### Develop a provider To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk) available under the OpenFeature organization. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. ```csharp @@ -237,7 +237,7 @@ public class MyProvider : FeatureProvider ### Develop a hook To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk) available under the OpenFeature organization. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. Implement your own hook by conforming to the `Hook interface`. To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. From 2687cf0663e20aa2dd113569cbf177833639cbbd Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Wed, 11 Oct 2023 16:41:03 -0400 Subject: [PATCH 100/316] docs: remove duplicate a tag from readme Signed-off-by: Michael Beemer --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 48c3af04..2cb0fd4b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ Release -
From 5dfea29bb3d01f6c8640de321c4fde52f283a1c0 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Thu, 12 Oct 2023 11:42:27 -0400 Subject: [PATCH 101/316] chore: add placeholder eventing and shutdown sections (#156) Signed-off-by: Michael Beemer --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2cb0fd4b..582b188a 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,14 @@ FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); ``` +### Eventing + +Events are currently not supported by the .NET SDK. Progress on this feature can be tracked [here](https://github.com/open-feature/dotnet-sdk/issues/126). + +### Shutdown + +A shutdown handler is not yet available in the .NET SDK. Progress on this feature can be tracked [here](https://github.com/open-feature/dotnet-sdk/issues/126). + ## Extending ### Develop a provider From 2cbdba80d836f8b7850e8dc5f1f1790ef2ed1aca Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Thu, 12 Oct 2023 11:46:32 -0400 Subject: [PATCH 102/316] chore: fix alt text for NuGet on the readme Signed-off-by: Michael Beemer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 582b188a..65007001 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Codecov - Codecov + NuGet CII Best Practices From a2f70ebd68357156f9045fc6e94845a53ffd204a Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 31 Oct 2023 15:31:06 -0400 Subject: [PATCH 103/316] chore: update spec release link Signed-off-by: Michael Beemer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65007001..97e8beaa 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

- + Specification From 24c344163423973b54a06b73648ba45b944589ee Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 14 Nov 2023 08:42:13 -0800 Subject: [PATCH 104/316] feat!: Add support for provider shutdown and status. (#158) Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: Todd Baert --- README.md | 17 +- src/OpenFeature/Api.cs | 66 +- src/OpenFeature/Constant/ProviderStatus.cs | 31 + src/OpenFeature/FeatureProvider.cs | 49 ++ src/OpenFeature/ProviderRepository.cs | 291 +++++++++ .../OpenFeatureClientBenchmarks.cs | 1 - .../Steps/EvaluationStepDefinitions.cs | 2 +- .../ClearOpenFeatureInstanceFixture.cs | 2 +- .../OpenFeatureClientTests.cs | 22 +- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 18 +- test/OpenFeature.Tests/OpenFeatureTests.cs | 71 ++- .../ProviderRepositoryTests.cs | 598 ++++++++++++++++++ 12 files changed, 1101 insertions(+), 67 deletions(-) create mode 100644 src/OpenFeature/Constant/ProviderStatus.cs create mode 100644 src/OpenFeature/ProviderRepository.cs create mode 100644 test/OpenFeature.Tests/ProviderRepositoryTests.cs diff --git a/README.md b/README.md index 97e8beaa..b625137b 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ dotnet add package OpenFeature public async Task Example() { // Register your feature flag provider - Api.Instance.SetProvider(new InMemoryProvider()); + await Api.Instance.SetProvider(new InMemoryProvider()); // Create a new client FeatureClient client = Api.Instance.GetClient(); @@ -97,7 +97,7 @@ public async Task Example() | βœ… | [Logging](#logging) | Integrate with popular logging packages. | | βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | | ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| ❌ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ @@ -112,7 +112,7 @@ 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 -Api.Instance.SetProvider(new MyProvider()); +await Api.Instance.SetProvider(new MyProvider()); ``` In some situations, it may be beneficial to register multiple providers in the same application. @@ -179,9 +179,9 @@ If a name has no associated provider, the global provider is used. ```csharp // registering the default provider -Api.Instance.SetProvider(new LocalProvider()); +await Api.Instance.SetProvider(new LocalProvider()); // registering a named provider -Api.Instance.SetProvider("clientForCache", new CachedProvider()); +await Api.Instance.SetProvider("clientForCache", new CachedProvider()); // a client backed by default provider FeatureClient clientDefault = Api.Instance.GetClient(); @@ -196,7 +196,12 @@ Events are currently not supported by the .NET SDK. Progress on this feature can ### Shutdown -A shutdown handler is not yet available in the .NET SDK. Progress on this feature can be tracked [here](https://github.com/open-feature/dotnet-sdk/issues/126). +The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. + +```csharp +// Shut down all providers +await Api.Instance.Shutdown(); +``` ## Extending diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index e679bf75..3583572e 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OpenFeature.Model; @@ -15,14 +16,12 @@ namespace OpenFeature public sealed class Api { private EvaluationContext _evaluationContext = EvaluationContext.Empty; - private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); - private readonly ConcurrentDictionary _featureProviders = - new ConcurrentDictionary(); + private readonly ProviderRepository _repository = new ProviderRepository(); private readonly ConcurrentStack _hooks = new ConcurrentStack(); /// The reader/writer locks are not disposed because the singleton instance should never be disposed. private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim(); - private readonly ReaderWriterLockSlim _featureProviderLock = new ReaderWriterLockSlim(); + ///

/// Singleton instance of Api @@ -36,31 +35,26 @@ static Api() { } private Api() { } /// - /// Sets the feature provider + /// Sets the 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. /// Implementation of - public void SetProvider(FeatureProvider featureProvider) + public async Task SetProvider(FeatureProvider featureProvider) { - this._featureProviderLock.EnterWriteLock(); - try - { - this._defaultProvider = featureProvider ?? this._defaultProvider; - } - finally - { - this._featureProviderLock.ExitWriteLock(); - } + await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); } + /// - /// Sets the feature provider to given clientName + /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and + /// initialization to complete, await the returned task. /// /// Name of client /// Implementation of - public void SetProvider(string clientName, FeatureProvider featureProvider) + public async Task SetProvider(string clientName, FeatureProvider featureProvider) { - this._featureProviders.AddOrUpdate(clientName, featureProvider, - (key, current) => featureProvider); + await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); } /// @@ -76,15 +70,7 @@ public void SetProvider(string clientName, FeatureProvider featureProvider) /// public FeatureProvider GetProvider() { - this._featureProviderLock.EnterReadLock(); - try - { - return this._defaultProvider; - } - finally - { - this._featureProviderLock.ExitReadLock(); - } + return this._repository.GetProvider(); } /// @@ -95,17 +81,9 @@ public FeatureProvider GetProvider() /// have a corresponding provider the default provider will be returned public FeatureProvider GetProvider(string clientName) { - if (string.IsNullOrEmpty(clientName)) - { - return this.GetProvider(); - } - - return this._featureProviders.TryGetValue(clientName, out var featureProvider) - ? featureProvider - : this.GetProvider(); + return this._repository.GetProvider(clientName); } - /// /// Gets providers metadata /// @@ -210,5 +188,19 @@ public EvaluationContext GetContext() 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 Shutdown() + { + await this._repository.Shutdown().ConfigureAwait(false); + } } } diff --git a/src/OpenFeature/Constant/ProviderStatus.cs b/src/OpenFeature/Constant/ProviderStatus.cs new file mode 100644 index 00000000..e56c6c95 --- /dev/null +++ b/src/OpenFeature/Constant/ProviderStatus.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; + +namespace OpenFeature.Constant +{ + /// + /// The state of the provider. + /// + /// + public enum ProviderStatus + { + /// + /// The provider has not been initialized and cannot yet evaluate flags. + /// + [Description("NOT_READY")] NotReady, + + /// + /// 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 is in an error state and unable to evaluate flags. + /// + [Description("ERROR")] Error + } +} diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index fe8f664d..c3cc1406 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Threading.Tasks; +using OpenFeature.Constant; using OpenFeature.Model; namespace OpenFeature @@ -79,5 +80,53 @@ public abstract Task> ResolveDoubleValue(string flagKe /// public abstract Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null); + + /// + /// Get the status of the provider. + /// + /// The current + /// + /// If a provider does not override this method, then its status will be assumed to be + /// . If a provider implements this method, and supports initialization, + /// then it should start in the status . If the status is + /// , then the Api will call the when the + /// provider is set. + /// + public virtual ProviderStatus GetStatus() => ProviderStatus.Ready; + + /// + /// + /// 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. + /// + /// + /// + /// A task that completes when the initialization process is complete. + /// + /// + /// A provider which supports initialization should override this method as well as + /// . + /// + /// + /// The provider should return or from + /// the method after initialization is complete. + /// + /// + public virtual Task Initialize(EvaluationContext context) + { + // 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. + public virtual Task Shutdown() + { + // Intentionally left blank. + return Task.CompletedTask; + } } } diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs new file mode 100644 index 00000000..dbd0794c --- /dev/null +++ b/src/OpenFeature/ProviderRepository.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Constant; +using OpenFeature.Model; + + +namespace OpenFeature +{ + /// + /// This class manages the collection of providers, both default and named, contained by the API. + /// + internal class ProviderRepository + { + private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); + + private readonly ConcurrentDictionary _featureProviders = + new ConcurrentDictionary(); + + /// The reader/writer locks is not disposed because the singleton instance should never be disposed. + /// + /// Mutations of the _defaultProvider or _featureProviders are done within this lock even though + /// _featureProvider is a concurrent collection. This is for a couple reasons, the first is that + /// a provider should only be shutdown if it is not in use, and it could be in use as either a named or + /// default provider. + /// + /// The second is that a concurrent collection doesn't provide any ordering so we could check a provider + /// as it was being added or removed such as two concurrent calls to SetProvider replacing multiple instances + /// of that provider under different names.. + private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); + + /// + /// Set the default provider + /// + /// the provider to set as the default, passing null has no effect + /// the context to initialize the provider with + /// + /// + /// Called after the provider is set, but before any actions are taken on it. + /// + /// This can be used for tasks such as registering event handlers. It should be noted that this can be called + /// several times for a single provider. For instance registering a provider with multiple names or as the + /// default and named provider. + /// + /// + /// + /// + /// called after the provider has initialized successfully, only called if the provider needed initialization + /// + /// + /// called if an error happens during the initialization of the provider, only called if the provider needed + /// initialization + /// + /// called after a provider is shutdown, can be used to remove event handlers + public async Task SetProvider( + FeatureProvider featureProvider, + EvaluationContext context, + Action afterSet = null, + Action afterInitialization = null, + Action afterError = null, + Action afterShutdown = null) + { + // Cannot unset the feature provider. + if (featureProvider == null) + { + return; + } + + this._providersLock.EnterWriteLock(); + // Default provider is swapped synchronously, initialization and shutdown may happen asynchronously. + try + { + // Setting the provider to the same provider should not have an effect. + if (ReferenceEquals(featureProvider, this._defaultProvider)) + { + return; + } + + var oldProvider = this._defaultProvider; + this._defaultProvider = featureProvider; + afterSet?.Invoke(featureProvider); + // We want to allow shutdown to happen concurrently with initialization, and the caller to not + // wait for it. +#pragma warning disable CS4014 + this.ShutdownIfUnused(oldProvider, afterShutdown, afterError); +#pragma warning restore CS4014 + } + finally + { + this._providersLock.ExitWriteLock(); + } + + await InitProvider(this._defaultProvider, context, afterInitialization, afterError) + .ConfigureAwait(false); + } + + private static async Task InitProvider( + FeatureProvider newProvider, + EvaluationContext context, + Action afterInitialization, + Action afterError) + { + if (newProvider == null) + { + return; + } + if (newProvider.GetStatus() == ProviderStatus.NotReady) + { + try + { + await newProvider.Initialize(context).ConfigureAwait(false); + afterInitialization?.Invoke(newProvider); + } + catch (Exception ex) + { + afterError?.Invoke(newProvider, ex); + } + } + } + + /// + /// Set a named provider + /// + /// the name to associate with the provider + /// the provider to set as the default, passing null has no effect + /// the context to initialize the provider with + /// + /// + /// Called after the provider is set, but before any actions are taken on it. + /// + /// This can be used for tasks such as registering event handlers. It should be noted that this can be called + /// several times for a single provider. For instance registering a provider with multiple names or as the + /// default and named provider. + /// + /// + /// + /// + /// called after the provider has initialized successfully, only called if the provider needed initialization + /// + /// + /// called if an error happens during the initialization of the provider, only called if the provider needed + /// initialization + /// + /// called after a provider is shutdown, can be used to remove event handlers + public async Task SetProvider(string clientName, + FeatureProvider featureProvider, + EvaluationContext context, + Action afterSet = null, + Action afterInitialization = null, + Action afterError = null, + Action afterShutdown = null) + { + // Cannot set a provider for a null clientName. + if (clientName == null) + { + return; + } + + this._providersLock.EnterWriteLock(); + + try + { + this._featureProviders.TryGetValue(clientName, out var oldProvider); + if (featureProvider != null) + { + this._featureProviders.AddOrUpdate(clientName, featureProvider, + (key, current) => featureProvider); + afterSet?.Invoke(featureProvider); + } + else + { + // If names of clients are programmatic, then setting the provider to null could result + // in unbounded growth of the collection. + this._featureProviders.TryRemove(clientName, out _); + } + + // We want to allow shutdown to happen concurrently with initialization, and the caller to not + // wait for it. +#pragma warning disable CS4014 + this.ShutdownIfUnused(oldProvider, afterShutdown, afterError); +#pragma warning restore CS4014 + } + finally + { + this._providersLock.ExitWriteLock(); + } + + await InitProvider(featureProvider, context, afterInitialization, afterError).ConfigureAwait(false); + } + + /// + /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. + /// + private async Task ShutdownIfUnused( + FeatureProvider targetProvider, + Action afterShutdown, + Action afterError) + { + if (ReferenceEquals(this._defaultProvider, targetProvider)) + { + return; + } + + if (this._featureProviders.Values.Contains(targetProvider)) + { + return; + } + + await SafeShutdownProvider(targetProvider, afterShutdown, afterError).ConfigureAwait(false); + } + + /// + /// + /// Shut down the provider and capture any exceptions thrown. + /// + /// + /// The provider is set either to a name or default before the old provider it shutdown, so + /// it would not be meaningful to emit an error. + /// + /// + private static async Task SafeShutdownProvider(FeatureProvider targetProvider, + Action afterShutdown, + Action afterError) + { + try + { + await targetProvider.Shutdown().ConfigureAwait(false); + afterShutdown?.Invoke(targetProvider); + } + catch (Exception ex) + { + afterError?.Invoke(targetProvider, ex); + } + } + + public FeatureProvider GetProvider() + { + this._providersLock.EnterReadLock(); + try + { + return this._defaultProvider; + } + finally + { + this._providersLock.ExitReadLock(); + } + } + + public FeatureProvider GetProvider(string clientName) + { + if (string.IsNullOrEmpty(clientName)) + { + return this.GetProvider(); + } + + return this._featureProviders.TryGetValue(clientName, out var featureProvider) + ? featureProvider + : this.GetProvider(); + } + + public async Task Shutdown(Action afterError = null) + { + var providers = new HashSet(); + this._providersLock.EnterWriteLock(); + try + { + providers.Add(this._defaultProvider); + foreach (var featureProvidersValue in this._featureProviders.Values) + { + providers.Add(featureProvidersValue); + } + + // Set a default provider so the Api is ready to be used again. + this._defaultProvider = new NoOpFeatureProvider(); + this._featureProviders.Clear(); + } + finally + { + this._providersLock.ExitWriteLock(); + } + + foreach (var targetProvider in providers) + { + // We don't need to take any actions after shutdown. + await SafeShutdownProvider(targetProvider, null, afterError).ConfigureAwait(false); + } + } + } +} diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 3adcb132..03f6082a 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -40,7 +40,6 @@ public OpenFeatureClientBenchmarks() _defaultStructureValue = fixture.Create(); _emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - Api.Instance.SetProvider(new NoOpFeatureProvider()); _client = Api.Instance.GetClient(_clientName, _clientVersion); } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index bb177468..4847bfb2 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -42,7 +42,7 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) { _scenarioContext = scenarioContext; var flagdProvider = new FlagdProvider(); - Api.Instance.SetProvider(flagdProvider); + Api.Instance.SetProvider(flagdProvider).Wait(); client = Api.Instance.GetClient(); } diff --git a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs index 3a0ab349..a70921f7 100644 --- a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs +++ b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs @@ -7,7 +7,7 @@ public ClearOpenFeatureInstanceFixture() { Api.Instance.SetContext(null); Api.Instance.ClearHooks(); - Api.Instance.SetProvider(new NoOpFeatureProvider()); + Api.Instance.SetProvider(new NoOpFeatureProvider()).Wait(); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 4995423e..30bee168 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -75,7 +75,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - Api.Instance.SetProvider(new NoOpFeatureProvider()); + await Api.Instance.SetProvider(new NoOpFeatureProvider()); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); @@ -121,7 +121,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - Api.Instance.SetProvider(new NoOpFeatureProvider()); + await Api.Instance.SetProvider(new NoOpFeatureProvider()); var client = Api.Instance.GetClient(clientName, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); @@ -172,7 +172,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedFeatureProvider.GetMetadata().Returns(new Metadata(fixture.Create())); mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(mockedFeatureProvider); + await Api.Instance.SetProvider(mockedFeatureProvider); var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger); var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); @@ -202,7 +202,7 @@ public async Task Should_Resolve_BooleanValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -224,7 +224,7 @@ public async Task Should_Resolve_StringValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetStringValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -246,7 +246,7 @@ public async Task Should_Resolve_IntegerValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -268,7 +268,7 @@ public async Task Should_Resolve_DoubleValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -290,7 +290,7 @@ public async Task Should_Resolve_StructureValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -313,7 +313,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); @@ -338,7 +338,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProvider(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); @@ -351,7 +351,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() [Fact] public async Task Should_Use_No_Op_When_Provider_Is_Null() { - Api.Instance.SetProvider(null); + await Api.Instance.SetProvider(null); var client = new FeatureClient("test", "test"); (await client.GetIntegerValue("some-key", 12)).Should().Be(12); } diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 72e4a1d0..a4810c04 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -51,7 +51,7 @@ public async Task Hooks_Should_Be_Called_In_Order() var testProvider = new TestProvider(); testProvider.AddHook(providerHook); Api.Instance.AddHooks(apiHook); - Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProvider(testProvider); var client = Api.Instance.GetClient(clientName, clientVersion); client.AddHooks(clientHook); @@ -197,7 +197,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() provider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); - Api.Instance.SetProvider(provider); + await Api.Instance.SetProvider(provider); var hook = Substitute.For(); hook.Before(Arg.Any>(), Arg.Any>()).Returns(hookContext); @@ -269,7 +269,7 @@ public async Task Hook_Should_Execute_In_Correct_Order() _ = hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); _ = hook.Finally(Arg.Any>(), Arg.Any>()); - Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook); @@ -301,7 +301,7 @@ public async Task Register_Hooks_Should_Be_Available_At_All_Levels() var testProvider = new TestProvider(); testProvider.AddHook(hook4); Api.Instance.AddHooks(hook1); - Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProvider(testProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook2); await client.GetBooleanValue("test", false, null, @@ -332,7 +332,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() hook2.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); hook1.Finally(Arg.Any>(), null).Throws(new Exception()); - Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); client.GetHooks().Count().Should().Be(2); @@ -377,7 +377,7 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() hook2.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); hook1.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); - Api.Instance.SetProvider(featureProvider1); + await Api.Instance.SetProvider(featureProvider1); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); @@ -414,7 +414,7 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ _ = hook1.Error(Arg.Any>(), Arg.Any(), null); _ = hook2.Error(Arg.Any>(), Arg.Any(), null); - Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); @@ -459,7 +459,7 @@ public async Task Hook_Hints_May_Be_Optional() hook.Finally(Arg.Any>(), Arg.Any>()) .Returns(Task.CompletedTask); - Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); await client.GetBooleanValue("test", false, EvaluationContext.Empty, flagOptions); @@ -537,7 +537,7 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() hook.Finally(Arg.Any>(), Arg.Any>()) .Returns(Task.CompletedTask); - Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProvider(featureProvider); var client = Api.Instance.GetClient(); var resolvedFlag = await client.GetBooleanValue("test", true, config: flagOptions); diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 9776d552..afcfcd18 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using NSubstitute; using OpenFeature.Constant; @@ -20,6 +21,74 @@ public void OpenFeature_Should_Be_Singleton() openFeature.Should().BeSameAs(openFeature2); } + [Fact] + [Specification("1.1.2.2", "The provider mutator function MUST invoke the initialize function on the newly registered provider before using it to resolve flag values.")] + public async Task OpenFeature_Should_Initialize_Provider() + { + var providerMockDefault = Substitute.For(); + providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProvider(providerMockDefault).ConfigureAwait(false); + await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + + var providerMockNamed = Substitute.For(); + providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProvider("the-name", providerMockNamed).ConfigureAwait(false); + await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + } + + [Fact] + [Specification("1.1.2.3", + "The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no longer being used to resolve flag values.")] + public async Task OpenFeature_Should_Shutdown_Unused_Provider() + { + var providerA = Substitute.For(); + providerA.GetStatus().Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProvider(providerA).ConfigureAwait(false); + await providerA.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + + var providerB = Substitute.For(); + providerB.GetStatus().Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProvider(providerB).ConfigureAwait(false); + await providerB.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + await providerA.Received(1).Shutdown().ConfigureAwait(false); + + var providerC = Substitute.For(); + providerC.GetStatus().Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProvider("named", providerC).ConfigureAwait(false); + await providerC.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + + var providerD = Substitute.For(); + providerD.GetStatus().Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProvider("named", providerD).ConfigureAwait(false); + await providerD.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + await providerC.Received(1).Shutdown().ConfigureAwait(false); + } + + [Fact] + [Specification("1.6.1", "The API MUST define a mechanism to propagate a shutdown request to active providers.")] + public async Task OpenFeature_Should_Support_Shutdown() + { + var providerA = Substitute.For(); + providerA.GetStatus().Returns(ProviderStatus.NotReady); + + var providerB = Substitute.For(); + providerB.GetStatus().Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProvider(providerA).ConfigureAwait(false); + await Api.Instance.SetProvider("named", providerB).ConfigureAwait(false); + + await Api.Instance.Shutdown().ConfigureAwait(false); + + await providerA.Received(1).Shutdown().ConfigureAwait(false); + await providerB.Received(1).Shutdown().ConfigureAwait(false); + } + [Fact] [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider() @@ -111,7 +180,7 @@ public void OpenFeature_Should_Add_Hooks() [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] public void OpenFeature_Should_Get_Metadata() { - Api.Instance.SetProvider(new NoOpFeatureProvider()); + Api.Instance.SetProvider(new NoOpFeatureProvider()).Wait(); var openFeature = Api.Instance; var metadata = openFeature.GetProviderMetadata(); diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs new file mode 100644 index 00000000..6d2ff310 --- /dev/null +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -0,0 +1,598 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using OpenFeature.Constant; +using OpenFeature.Model; +using Xunit; + +// We intentionally do not await for purposes of validating behavior. +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + +namespace OpenFeature.Tests +{ + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] + public class ProviderRepositoryTests + { + [Fact] + public void Default_Provider_Is_Set_Without_Await() + { + var repository = new ProviderRepository(); + var provider = new NoOpFeatureProvider(); + var context = new EvaluationContextBuilder().Build(); + repository.SetProvider(provider, context); + Assert.Equal(provider, repository.GetProvider()); + } + + [Fact] + public void AfterSet_Is_Invoked_For_Setting_Default_Provider() + { + var repository = new ProviderRepository(); + var provider = new NoOpFeatureProvider(); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + // The setting of the provider is synchronous, so the afterSet should be as well. + repository.SetProvider(provider, context, afterSet: (theProvider) => + { + callCount++; + Assert.Equal(provider, theProvider); + }); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Default_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(providerMock, context); + providerMock.Received(1).Initialize(context); + providerMock.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task AfterInitialization_Is_Invoked_For_Setting_Default_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProvider(providerMock, context, afterInitialization: (theProvider) => + { + Assert.Equal(providerMock, theProvider); + callCount++; + }); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); + var callCount = 0; + Exception receivedError = null; + await repository.SetProvider(providerMock, context, afterError: (theProvider, error) => + { + Assert.Equal(providerMock, theProvider); + callCount++; + receivedError = error; + }); + Assert.Equal("BAD THINGS", receivedError.Message); + Assert.Equal(1, callCount); + } + + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + public async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(status); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(providerMock, context); + providerMock.DidNotReceive().Initialize(context); + } + + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + public async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(status); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProvider(providerMock, context, afterInitialization: provider => { callCount++; }); + Assert.Equal(0, callCount); + } + + [Fact] + public async Task Replaced_Default_Provider_Is_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(provider1, context); + await repository.SetProvider(provider2, context); + provider1.Received(1).Shutdown(); + provider2.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task AfterShutdown_Is_Called_For_Shutdown_Provider() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(provider1, context); + var callCount = 0; + await repository.SetProvider(provider2, context, afterShutdown: provider => + { + Assert.Equal(provider, provider1); + callCount++; + }); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task AfterError_Is_Called_For_Shutdown_That_Throws() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR")); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(provider1, context); + var callCount = 0; + Exception errorThrown = null; + await repository.SetProvider(provider2, context, afterError: (provider, ex) => + { + Assert.Equal(provider, provider1); + errorThrown = ex; + callCount++; + }); + Assert.Equal(1, callCount); + Assert.Equal("SHUTDOWN ERROR", errorThrown.Message); + } + + [Fact] + public void Named_Provider_Provider_Is_Set_Without_Await() + { + var repository = new ProviderRepository(); + var provider = new NoOpFeatureProvider(); + var context = new EvaluationContextBuilder().Build(); + repository.SetProvider("the-name", provider, context); + Assert.Equal(provider, repository.GetProvider("the-name")); + } + + [Fact] + public void AfterSet_Is_Invoked_For_Setting_Named_Provider() + { + var repository = new ProviderRepository(); + var provider = new NoOpFeatureProvider(); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + // The setting of the provider is synchronous, so the afterSet should be as well. + repository.SetProvider("the-name", provider, context, afterSet: (theProvider) => + { + callCount++; + Assert.Equal(provider, theProvider); + }); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Named_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider("the-name", providerMock, context); + providerMock.Received(1).Initialize(context); + providerMock.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task AfterInitialization_Is_Invoked_For_Setting_Named_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProvider("the-name", providerMock, context, afterInitialization: (theProvider) => + { + Assert.Equal(providerMock, theProvider); + callCount++; + }); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); + var callCount = 0; + Exception receivedError = null; + await repository.SetProvider("the-provider", providerMock, context, afterError: (theProvider, error) => + { + Assert.Equal(providerMock, theProvider); + callCount++; + receivedError = error; + }); + Assert.Equal("BAD THINGS", receivedError.Message); + Assert.Equal(1, callCount); + } + + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + public async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(status); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider("the-name", providerMock, context); + providerMock.DidNotReceive().Initialize(context); + } + + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + public async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.GetStatus().Returns(status); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProvider("the-name", providerMock, context, + afterInitialization: provider => { callCount++; }); + Assert.Equal(0, callCount); + } + + [Fact] + public async Task Replaced_Named_Provider_Is_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider("the-name", provider1, context); + await repository.SetProvider("the-name", provider2, context); + provider1.Received(1).Shutdown(); + provider2.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task AfterShutdown_Is_Called_For_Shutdown_Named_Provider() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider("the-provider", provider1, context); + var callCount = 0; + await repository.SetProvider("the-provider", provider2, context, afterShutdown: provider => + { + Assert.Equal(provider, provider1); + callCount++; + }); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR")); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider("the-name", provider1, context); + var callCount = 0; + Exception errorThrown = null; + await repository.SetProvider("the-name", provider2, context, afterError: (provider, ex) => + { + Assert.Equal(provider, provider1); + errorThrown = ex; + callCount++; + }); + Assert.Equal(1, callCount); + Assert.Equal("SHUTDOWN ERROR", errorThrown.Message); + } + + [Fact] + public async Task In_Use_Provider_Named_And_Default_Is_Not_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProvider(provider1, context); + await repository.SetProvider("A", provider1, context); + // Provider one is replaced for "A", but not default. + await repository.SetProvider("A", provider2, context); + + provider1.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task In_Use_Provider_Two_Named_Is_Not_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProvider("B", provider1, context); + await repository.SetProvider("A", provider1, context); + // Provider one is replaced for "A", but not "B". + await repository.SetProvider("A", provider2, context); + + provider1.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task When_All_Instances_Are_Removed_Shutdown_Is_Called() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProvider("B", provider1, context); + await repository.SetProvider("A", provider1, context); + + await repository.SetProvider("A", provider2, context); + await repository.SetProvider("B", provider2, context); + + provider1.Received(1).Shutdown(); + } + + [Fact] + public async Task Can_Get_Providers_By_Name() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProvider("A", provider1, context); + await repository.SetProvider("B", provider2, context); + + Assert.Equal(provider1, repository.GetProvider("A")); + Assert.Equal(provider2, repository.GetProvider("B")); + } + + [Fact] + public async Task Replaced_Named_Provider_Gets_Latest_Set() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProvider("A", provider1, context); + await repository.SetProvider("A", provider2, context); + + Assert.Equal(provider2, repository.GetProvider("A")); + } + + [Fact] + public async Task Can_Shutdown_All_Providers() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + + var provider3 = Substitute.For(); + provider3.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProvider(provider1, context); + await repository.SetProvider("provider1", provider1, context); + await repository.SetProvider("provider2", provider2, context); + await repository.SetProvider("provider2a", provider2, context); + await repository.SetProvider("provider3", provider3, context); + + await repository.Shutdown(); + + provider1.Received(1).Shutdown(); + provider2.Received(1).Shutdown(); + provider3.Received(1).Shutdown(); + } + + [Fact] + public async Task Errors_During_Shutdown_Propagate() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR 1")); + + var provider2 = Substitute.For(); + provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Shutdown().Throws(new Exception("SHUTDOWN ERROR 2")); + + var provider3 = Substitute.For(); + provider3.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProvider(provider1, context); + await repository.SetProvider("provider1", provider1, context); + await repository.SetProvider("provider2", provider2, context); + await repository.SetProvider("provider2a", provider2, context); + await repository.SetProvider("provider3", provider3, context); + + var callCountShutdown1 = 0; + var callCountShutdown2 = 0; + var totalCallCount = 0; + await repository.Shutdown(afterError: (provider, exception) => + { + totalCallCount++; + if (provider == provider1) + { + callCountShutdown1++; + Assert.Equal("SHUTDOWN ERROR 1", exception.Message); + } + + if (provider == provider2) + { + callCountShutdown2++; + Assert.Equal("SHUTDOWN ERROR 2", exception.Message); + } + }); + Assert.Equal(2, totalCallCount); + Assert.Equal(1, callCountShutdown1); + Assert.Equal(1, callCountShutdown2); + + provider1.Received(1).Shutdown(); + provider2.Received(1).Shutdown(); + provider3.Received(1).Shutdown(); + } + + [Fact] + public async Task Setting_Same_Default_Provider_Has_No_Effect() + { + var repository = new ProviderRepository(); + var provider = Substitute.For(); + provider.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(provider, context); + await repository.SetProvider(provider, context); + + Assert.Equal(provider, repository.GetProvider()); + provider.Received(1).Initialize(context); + provider.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task Setting_Null_Default_Provider_Has_No_Effect() + { + var repository = new ProviderRepository(); + var provider = Substitute.For(); + provider.GetStatus().Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(provider, context); + await repository.SetProvider(null, context); + + Assert.Equal(provider, repository.GetProvider()); + provider.Received(1).Initialize(context); + provider.DidNotReceive().Shutdown(); + } + + [Fact] + public async Task Setting_Null_Named_Provider_Removes_It() + { + var repository = new ProviderRepository(); + + var namedProvider = Substitute.For(); + namedProvider.GetStatus().Returns(ProviderStatus.NotReady); + + var defaultProvider = Substitute.For(); + defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProvider(defaultProvider, context); + + await repository.SetProvider("named-provider", namedProvider, context); + await repository.SetProvider("named-provider", null, context); + + Assert.Equal(defaultProvider, repository.GetProvider("named-provider")); + } + + [Fact] + public async Task Setting_Named_Provider_With_Null_Name_Has_No_Effect() + { + var repository = new ProviderRepository(); + var context = new EvaluationContextBuilder().Build(); + + var defaultProvider = Substitute.For(); + defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); + await repository.SetProvider(defaultProvider, context); + + var namedProvider = Substitute.For(); + namedProvider.GetStatus().Returns(ProviderStatus.NotReady); + + await repository.SetProvider(null, namedProvider, context); + + namedProvider.DidNotReceive().Initialize(context); + namedProvider.DidNotReceive().Shutdown(); + + Assert.Equal(defaultProvider, repository.GetProvider(null)); + } + } +} From b6b0ee2b61a9b0b973b913b53887badfa0c5a3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:44:18 +0000 Subject: [PATCH 105/316] docs: Add README.md to the nuget package (#164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/OpenFeature.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index e04d063d..271b8ec2 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -3,6 +3,7 @@ netstandard2.0;net462 OpenFeature + README.md @@ -19,6 +20,7 @@ <_Parameter1>OpenFeature.E2ETests + - + \ No newline at end of file From c85e93e9c9a97083660f9062c38dcbf6d64a3ad6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 12:57:35 -0500 Subject: [PATCH 106/316] chore(deps): update github/codeql-action action to v3 (#163) 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 008a42f0..f24c8fe2 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@v2 + uses: github/codeql-action/init@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@v2 + uses: github/codeql-action/autobuild@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@v2 + uses: github/codeql-action/analyze@v3 From 90d9d021b227fba626bb99454cb7c0f7fef2d8d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 13:01:59 -0500 Subject: [PATCH 107/316] chore(deps): update actions/checkout action to v4 (#144) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d76b1ff7..91274672 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: unit-tests-linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -31,7 +31,7 @@ jobs: unit-tests-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 75ee00db..1ffd1013 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -17,7 +17,7 @@ jobs: OS: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f24c8fe2..6968bd71 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 5616a74d..91346abf 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .NET Core 7.0 uses: actions/setup-dotnet@v3 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 336bd702..e37b9359 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -19,7 +19,7 @@ jobs: ports: - 8013:8013 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74e5990e..e58b6a23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: token: ${{secrets.GITHUB_TOKEN}} default-branch: main - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: ${{ steps.release.outputs.releases_created }} with: fetch-depth: 0 From 0b0bb10419f836d9cc276fe8ac3c71c9214420ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 22:44:30 +1000 Subject: [PATCH 108/316] chore(deps): update actions/setup-dotnet action to v4 (#162) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91274672..51b3f741 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - name: Setup .NET Core 6.0.x, 7.0.x - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x @@ -36,7 +36,7 @@ jobs: fetch-depth: 0 - name: Setup .NET Core 6.0.x, 7.0.x - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 1ffd1013..e9077dcb 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup .NET Core 7.0.x - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '7.0.x' diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 91346abf..ad691216 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET Core 7.0 - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 7.0.x diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e37b9359..3b00d994 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Setup .NET Core 6.0.x, 7.0.x - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e58b6a23..644342a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: - name: Setup .NET Core 6.0.x, 7.0.x if: ${{ steps.release.outputs.releases_created }} - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x From e8ca1da9ed63df9685ec49a9569e0ec99ba0b3b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 22:44:54 +1000 Subject: [PATCH 109/316] chore(deps): update dependency dotnet-sdk to v7.0.404 (#148) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index aeb8ae7b..7664239a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.400" + "version": "7.0.404" } } From d0c25af7df5176d10088c148eac35b0034536e04 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Fri, 5 Jan 2024 17:04:43 -0500 Subject: [PATCH 110/316] chore: minor formatting cleanup (#168) Signed-off-by: Austin Drenski --- README.md | 86 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index b625137b..d8ed85a6 100644 --- a/README.md +++ b/README.md @@ -70,21 +70,21 @@ dotnet add package OpenFeature ```csharp public async Task Example() - { - // Register your feature flag provider - await Api.Instance.SetProvider(new InMemoryProvider()); +{ + // Register your feature flag provider + await Api.Instance.SetProvider(new InMemoryProvider()); - // Create a new client - FeatureClient client = Api.Instance.GetClient(); + // Create a new client + FeatureClient client = Api.Instance.GetClient(); - // Evaluate your feature flag - bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); - if ( v2Enabled ) - { - //Do some work - } - } + if ( v2Enabled ) + { + //Do some work + } +} ``` ## 🌟 Features @@ -180,11 +180,13 @@ If a name has no associated provider, the global provider is used. ```csharp // registering the default provider await Api.Instance.SetProvider(new LocalProvider()); + // registering a named provider await Api.Instance.SetProvider("clientForCache", new CachedProvider()); // a client backed by default provider - FeatureClient clientDefault = Api.Instance.GetClient(); +FeatureClient clientDefault = Api.Instance.GetClient(); + // a client backed by CachedProvider FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); @@ -213,37 +215,37 @@ You’ll then need to write the provider by implementing the `FeatureProvider` i ```csharp public class MyProvider : FeatureProvider +{ + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) { - public override Metadata GetMetadata() - { - return new Metadata("My Provider"); - } - - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) - { - // resolve a boolean flag value - } - - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) - { - // resolve a double flag value - } - - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) - { - // resolve an int flag value - } - - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) - { - // resolve a string flag value - } - - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) - { - // resolve an object flag value - } + // resolve a boolean flag value } + + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + { + // resolve a double flag value + } + + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + { + // resolve a string flag value + } + + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + { + // resolve an object flag value + } +} ``` ### Develop a hook From f5fc1ddadc11f712ae0893cde815e7a1c6fe2c1b Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 16 Jan 2024 19:52:56 +0100 Subject: [PATCH 111/316] feat: add support for eventing (#166) Signed-off-by: Florian Bacher --- README.md | 39 +- build/Common.props | 1 + src/OpenFeature/Api.cs | 29 +- src/OpenFeature/Constant/EventType.cs | 25 + src/OpenFeature/EventExecutor.cs | 371 +++++++++++++++ src/OpenFeature/FeatureProvider.cs | 14 +- src/OpenFeature/IEventBus.cs | 24 + src/OpenFeature/IFeatureClient.cs | 3 +- src/OpenFeature/Model/ProviderEvents.cs | 41 ++ src/OpenFeature/OpenFeatureClient.cs | 12 + .../OpenFeature.Tests/FeatureProviderTests.cs | 2 +- .../OpenFeatureEventTests.cs | 445 ++++++++++++++++++ test/OpenFeature.Tests/OpenFeatureTests.cs | 16 +- test/OpenFeature.Tests/TestImplementations.cs | 43 +- 14 files changed, 1054 insertions(+), 11 deletions(-) create mode 100644 src/OpenFeature/Constant/EventType.cs create mode 100644 src/OpenFeature/EventExecutor.cs create mode 100644 src/OpenFeature/IEventBus.cs create mode 100644 src/OpenFeature/Model/ProviderEvents.cs create mode 100644 test/OpenFeature.Tests/OpenFeatureEventTests.cs diff --git a/README.md b/README.md index d8ed85a6..f504211c 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ public async Task Example() | βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | | βœ… | [Logging](#logging) | Integrate with popular logging packages. | | βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | | βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | @@ -167,6 +167,43 @@ client.AddHooks(new ExampleClientHook()); var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); ``` +### Eventing + +Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, +provider readiness, or error conditions. +Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. +Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. + +Please refer to the documentation of the provider you're using to see what events are supported. + +Example usage of an Event handler: + +```csharp +public static void EventHandler(ProviderEventPayload eventDetails) +{ + Console.WriteLine(eventDetails.Type); +} +``` + +```csharp +EventHandlerDelegate callback = EventHandler; +// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event +Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +It is also possible to register an event handler for a specific client, as in the following example: + +```csharp +EventHandlerDelegate callback = EventHandler; + +var myClient = Api.Instance.GetClient("my-client"); + +var provider = new ExampleProvider(); +await Api.Instance.SetProvider(myClient.GetMetadata().Name, provider); + +myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + ### Logging The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. diff --git a/build/Common.props b/build/Common.props index 4af79a3c..42a91d64 100644 --- a/build/Common.props +++ b/build/Common.props @@ -25,5 +25,6 @@ + diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 3583572e..8d0e22f5 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using OpenFeature.Constant; using OpenFeature.Model; namespace OpenFeature @@ -13,7 +14,7 @@ namespace OpenFeature /// 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 + public sealed class Api : IEventBus { private EvaluationContext _evaluationContext = EvaluationContext.Empty; private readonly ProviderRepository _repository = new ProviderRepository(); @@ -22,6 +23,8 @@ public sealed class Api /// The reader/writer locks are not disposed because the singleton instance should never be disposed. private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim(); + internal readonly EventExecutor EventExecutor = new EventExecutor(); + /// /// Singleton instance of Api @@ -42,6 +45,7 @@ private Api() { } /// Implementation of public async Task SetProvider(FeatureProvider featureProvider) { + this.EventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); } @@ -54,6 +58,7 @@ public async Task SetProvider(FeatureProvider featureProvider) /// Implementation of public async Task SetProvider(string clientName, FeatureProvider featureProvider) { + this.EventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); } @@ -201,6 +206,28 @@ public EvaluationContext GetContext() public async Task Shutdown() { await this._repository.Shutdown().ConfigureAwait(false); + await this.EventExecutor.Shutdown().ConfigureAwait(false); + } + + /// + public void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler) + { + this.EventExecutor.AddApiLevelHandler(type, handler); + } + + /// + public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) + { + this.EventExecutor.RemoveApiLevelHandler(type, handler); + } + + /// + /// Sets the logger for the API + /// + /// The logger to be used + public void SetLogger(ILogger logger) + { + this.EventExecutor.Logger = logger; } } } diff --git a/src/OpenFeature/Constant/EventType.cs b/src/OpenFeature/Constant/EventType.cs new file mode 100644 index 00000000..3d3c9dc8 --- /dev/null +++ b/src/OpenFeature/Constant/EventType.cs @@ -0,0 +1,25 @@ +namespace OpenFeature.Constant +{ + /// + /// The ProviderEventTypes enum represents the available event types of a provider. + /// + 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 + } +} diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs new file mode 100644 index 00000000..8a6df9a4 --- /dev/null +++ b/src/OpenFeature/EventExecutor.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using OpenFeature.Constant; +using OpenFeature.Model; + +namespace OpenFeature +{ + + internal delegate Task ShutdownDelegate(); + + internal class EventExecutor + { + private readonly object _lockObj = new object(); + public readonly Channel EventChannel = Channel.CreateBounded(1); + private FeatureProviderReference _defaultProvider; + private readonly Dictionary _namedProviderReferences = new Dictionary(); + private readonly List _activeSubscriptions = new List(); + private readonly SemaphoreSlim _shutdownSemaphore = new SemaphoreSlim(0); + + private ShutdownDelegate _shutdownDelegate; + + private readonly Dictionary> _apiHandlers = new Dictionary>(); + private readonly Dictionary>> _clientHandlers = new Dictionary>>(); + + internal ILogger Logger { get; set; } + + public EventExecutor() + { + this.Logger = new Logger(new NullLoggerFactory()); + this._shutdownDelegate = this.SignalShutdownAsync; + var eventProcessing = new Thread(this.ProcessEventAsync); + eventProcessing.Start(); + } + + internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) + { + lock (this._lockObj) + { + if (!this._apiHandlers.TryGetValue(eventType, out var eventHandlers)) + { + eventHandlers = new List(); + this._apiHandlers[eventType] = eventHandlers; + } + + eventHandlers.Add(handler); + + this.EmitOnRegistration(this._defaultProvider, eventType, handler); + } + } + + internal void RemoveApiLevelHandler(ProviderEventTypes type, EventHandlerDelegate handler) + { + lock (this._lockObj) + { + if (this._apiHandlers.TryGetValue(type, out var eventHandlers)) + { + eventHandlers.Remove(handler); + } + } + } + + internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler) + { + 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)) + { + registry = new Dictionary>(); + this._clientHandlers[client] = registry; + } + + if (!this._clientHandlers[client].TryGetValue(eventType, out var eventHandlers)) + { + eventHandlers = new List(); + this._clientHandlers[client][eventType] = eventHandlers; + } + + this._clientHandlers[client][eventType].Add(handler); + + this.EmitOnRegistration( + this._namedProviderReferences.TryGetValue(client, out var clientProviderReference) + ? clientProviderReference + : this._defaultProvider, eventType, handler); + } + } + + internal void RemoveClientHandler(string client, ProviderEventTypes type, EventHandlerDelegate handler) + { + lock (this._lockObj) + { + if (this._clientHandlers.TryGetValue(client, out var clientEventHandlers)) + { + if (clientEventHandlers.TryGetValue(type, out var eventHandlers)) + { + eventHandlers.Remove(handler); + } + } + } + } + + internal void RegisterDefaultFeatureProvider(FeatureProvider provider) + { + if (provider == null) + { + return; + } + lock (this._lockObj) + { + var oldProvider = this._defaultProvider; + + this._defaultProvider = new FeatureProviderReference(provider); + + this.StartListeningAndShutdownOld(this._defaultProvider, oldProvider); + } + } + + internal void RegisterClientFeatureProvider(string client, FeatureProvider provider) + { + if (provider == null) + { + return; + } + lock (this._lockObj) + { + var newProvider = new FeatureProviderReference(provider); + FeatureProviderReference oldProvider = null; + if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider)) + { + oldProvider = foundOldProvider; + } + + this._namedProviderReferences[client] = newProvider; + + this.StartListeningAndShutdownOld(newProvider, oldProvider); + } + } + + private void StartListeningAndShutdownOld(FeatureProviderReference newProvider, FeatureProviderReference oldProvider) + { + // 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); + var featureProviderEventProcessing = new Thread(this.ProcessFeatureProviderEventsAsync); + featureProviderEventProcessing.Start(newProvider); + } + + if (oldProvider != null && !this.IsProviderBound(oldProvider)) + { + this._activeSubscriptions.Remove(oldProvider); + var channel = oldProvider.Provider.GetEventChannel(); + if (channel != null) + { + channel.Writer.WriteAsync(new ShutdownSignal()); + } + } + } + + private bool IsProviderBound(FeatureProviderReference provider) + { + if (this._defaultProvider == provider) + { + return true; + } + foreach (var providerReference in this._namedProviderReferences.Values) + { + if (providerReference == provider) + { + return true; + } + } + return false; + } + + private bool IsProviderActive(FeatureProviderReference providerRef) + { + return this._activeSubscriptions.Contains(providerRef); + } + + private void EmitOnRegistration(FeatureProviderReference provider, ProviderEventTypes eventType, EventHandlerDelegate handler) + { + if (provider == null) + { + return; + } + var status = provider.Provider.GetStatus(); + + var message = ""; + if (status == ProviderStatus.Ready && eventType == ProviderEventTypes.ProviderReady) + { + message = "Provider is ready"; + } + else if (status == ProviderStatus.Error && eventType == ProviderEventTypes.ProviderError) + { + message = "Provider is in error state"; + } + else if (status == ProviderStatus.Stale && eventType == ProviderEventTypes.ProviderStale) + { + message = "Provider is in stale state"; + } + + if (message != "") + { + try + { + handler.Invoke(new ProviderEventPayload + { + ProviderName = provider.Provider?.GetMetadata()?.Name, + Type = eventType, + Message = message + }); + } + catch (Exception exc) + { + this.Logger?.LogError("Error running handler: " + exc); + } + } + } + + private async void ProcessFeatureProviderEventsAsync(object providerRef) + { + while (true) + { + var typedProviderRef = (FeatureProviderReference)providerRef; + if (typedProviderRef.Provider.GetEventChannel() == null) + { + return; + } + var item = await typedProviderRef.Provider.GetEventChannel().Reader.ReadAsync().ConfigureAwait(false); + + switch (item) + { + case ProviderEventPayload eventPayload: + await this.EventChannel.Writer.WriteAsync(new Event { Provider = typedProviderRef, EventPayload = eventPayload }).ConfigureAwait(false); + break; + case ShutdownSignal _: + typedProviderRef.ShutdownSemaphore.Release(); + return; + } + } + } + + // Method to process events + private async void ProcessEventAsync() + { + while (true) + { + var item = await this.EventChannel.Reader.ReadAsync().ConfigureAwait(false); + + switch (item) + { + case Event e: + lock (this._lockObj) + { + if (this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers)) + { + foreach (var eventHandler in eventHandlers) + { + this.InvokeEventHandler(eventHandler, e); + } + } + + // look for client handlers and call invoke method there + foreach (var keyAndValue in this._namedProviderReferences) + { + if (keyAndValue.Value == e.Provider) + { + if (this._clientHandlers.TryGetValue(keyAndValue.Key, out var clientRegistry)) + { + if (clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) + { + foreach (var eventHandler in clientEventHandlers) + { + this.InvokeEventHandler(eventHandler, e); + } + } + } + } + } + + if (e.Provider != this._defaultProvider) + { + break; + } + // handling the default provider - invoke event handlers for clients which are not bound + // to a particular feature provider + foreach (var keyAndValues in this._clientHandlers) + { + if (this._namedProviderReferences.TryGetValue(keyAndValues.Key, out _)) + { + // if there is an association for the client to a specific feature provider, then continue + continue; + } + if (keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) + { + foreach (var eventHandler in clientEventHandlers) + { + this.InvokeEventHandler(eventHandler, e); + } + } + } + } + break; + case ShutdownSignal _: + this._shutdownSemaphore.Release(); + return; + } + + } + } + + private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) + { + try + { + eventHandler.Invoke(e.EventPayload); + } + catch (Exception exc) + { + this.Logger?.LogError("Error running handler: " + exc); + } + } + + public async Task Shutdown() + { + await this._shutdownDelegate().ConfigureAwait(false); + } + + internal void SetShutdownDelegate(ShutdownDelegate del) + { + this._shutdownDelegate = del; + } + + // Method to signal shutdown + private async Task SignalShutdownAsync() + { + // Enqueue a shutdown signal + await this.EventChannel.Writer.WriteAsync(new ShutdownSignal()).ConfigureAwait(false); + + // Wait for the processing loop to acknowledge the shutdown + await this._shutdownSemaphore.WaitAsync().ConfigureAwait(false); + } + } + + internal class ShutdownSignal + { + } + + internal class FeatureProviderReference + { + internal readonly SemaphoreSlim ShutdownSemaphore = new SemaphoreSlim(0); + internal FeatureProvider Provider { get; } + + public FeatureProviderReference(FeatureProvider provider) + { + this.Provider = provider; + } + } + + internal class Event + { + internal FeatureProviderReference Provider { get; set; } + internal ProviderEventPayload EventPayload { get; set; } + } +} diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index c3cc1406..3dd85102 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Threading.Channels; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; @@ -25,6 +26,11 @@ public abstract class FeatureProvider /// Immutable list of hooks public virtual IImmutableList GetProviderHooks() => ImmutableList.Empty; + /// + /// The event channel of the provider. + /// + protected Channel EventChannel = Channel.CreateBounded(1); + /// /// Metadata describing the provider. /// @@ -105,7 +111,7 @@ public abstract Task> ResolveStructureValue(string flag /// /// /// A provider which supports initialization should override this method as well as - /// . + /// . /// /// /// The provider should return or from @@ -128,5 +134,11 @@ public virtual Task Shutdown() // Intentionally left blank. return Task.CompletedTask; } + + /// + /// Returns the event channel of the provider. + /// + /// The event channel of the provider + public virtual Channel GetEventChannel() => this.EventChannel; } } diff --git a/src/OpenFeature/IEventBus.cs b/src/OpenFeature/IEventBus.cs new file mode 100644 index 00000000..114b66b3 --- /dev/null +++ b/src/OpenFeature/IEventBus.cs @@ -0,0 +1,24 @@ +using OpenFeature.Constant; +using OpenFeature.Model; + +namespace OpenFeature +{ + /// + /// Defines the methods required for handling events. + /// + 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); + } +} diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index 2186ca68..1d2e6dfb 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using OpenFeature.Constant; using OpenFeature.Model; namespace OpenFeature @@ -7,7 +8,7 @@ namespace OpenFeature /// /// Interface used to resolve flags of varying types. /// - public interface IFeatureClient + public interface IFeatureClient : IEventBus { /// /// Appends hooks to client diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs new file mode 100644 index 00000000..da68aef4 --- /dev/null +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using OpenFeature.Constant; + +namespace OpenFeature.Model +{ + /// + /// The EventHandlerDelegate is an implementation of an Event Handler + /// + public delegate void EventHandlerDelegate(ProviderEventPayload eventDetails); + + /// + /// Contains the payload of an OpenFeature Event. + /// + public class ProviderEventPayload + { + /// + /// Name of the provider. + /// + public string ProviderName { get; set; } + + /// + /// Type of the event + /// + public ProviderEventTypes Type { get; set; } + + /// + /// A message providing more information about the event. + /// + public string Message { get; set; } + + /// + /// A List of flags that have been changed. + /// + public List FlagsChanged { get; set; } + + /// + /// Metadata information for the event. + /// + public Dictionary EventMetadata { get; set; } + } +} diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 1cc26802..9ea9b13a 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -93,6 +93,18 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// Hook that implements the interface public void AddHooks(Hook hook) => this._hooks.Push(hook); + /// + public void AddHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) + { + Api.Instance.EventExecutor.AddClientHandler(this._metadata.Name, eventType, handler); + } + + /// + public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) + { + Api.Instance.EventExecutor.RemoveClientHandler(this._metadata.Name, type, handler); + } + /// public void AddHooks(IEnumerable hooks) => this._hooks.PushRange(hooks.ToArray()); diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index aa79cbf9..8d679f94 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -19,7 +19,7 @@ public void Provider_Must_Have_Metadata() { var provider = new TestProvider(); - provider.GetMetadata().Name.Should().Be(TestProvider.Name); + provider.GetMetadata().Name.Should().Be(TestProvider.DefaultName); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs new file mode 100644 index 00000000..8c183e63 --- /dev/null +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using NSubstitute; +using OpenFeature.Constant; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; +using Xunit; + +namespace OpenFeature.Tests +{ + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] + public class OpenFeatureEventTest : ClearOpenFeatureInstanceFixture + { + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() + { + var eventHandler = Substitute.For(); + + var eventExecutor = new EventExecutor(); + + eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); + + var eventMetadata = new Dictionary { { "foo", "bar" } }; + var myEvent = new Event + { + EventPayload = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderConfigurationChanged, + Message = "The provider is ready", + EventMetadata = eventMetadata, + FlagsChanged = new List { "flag1", "flag2" } + } + }; + eventExecutor.EventChannel.Writer.TryWrite(myEvent); + + Thread.Sleep(1000); + + eventHandler.Received().Invoke(Arg.Is(payload => payload == myEvent.EventPayload)); + + // shut down the event executor + await eventExecutor.Shutdown(); + + // the next event should not be propagated to the event handler + var newEventPayload = new ProviderEventPayload { Type = ProviderEventTypes.ProviderStale }; + + eventExecutor.EventChannel.Writer.TryWrite(newEventPayload); + + eventHandler.DidNotReceive().Invoke(newEventPayload); + + eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.Type == ProviderEventTypes.ProviderStale)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + public async Task API_Level_Event_Handlers_Should_Be_Registered() + { + var eventHandler = Substitute.For(); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(testProvider); + + testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + testProvider.SendEvent(ProviderEventTypes.ProviderError); + testProvider.SendEvent(ProviderEventTypes.ProviderStale); + + Thread.Sleep(1000); + eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady + )); + + eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged + )); + + eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError + )); + + eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale + )); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Ready() + { + var eventHandler = Substitute.For(); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(testProvider); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + Thread.Sleep(1000); + eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady + )); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.2", "If the provider's `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_After_Registering_Provider_Error() + { + var eventHandler = Substitute.For(); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(testProvider); + + testProvider.SetStatus(ProviderStatus.Error); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); + + Thread.Sleep(1000); + eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError + )); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_After_Registering_Provider_Stale() + { + var eventHandler = Substitute.For(); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(testProvider); + + testProvider.SetStatus(ProviderStatus.Stale); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); + + Thread.Sleep(1000); + eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale + )); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")] + public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() + { + var eventHandler = Substitute.For(); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(testProvider); + + testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + + var newTestProvider = new TestProvider(); + await Api.Instance.SetProvider(newTestProvider); + + newTestProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + + Thread.Sleep(1000); + eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)); + eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + } + + [Fact] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")] + public async Task API_Level_Event_Handlers_Should_Be_Removable() + { + var eventHandler = Substitute.For(); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(testProvider); + + Thread.Sleep(1000); + Api.Instance.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var newTestProvider = new TestProvider(); + await Api.Instance.SetProvider(newTestProvider); + + eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] + public async Task API_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() + { + var fixture = new Fixture(); + + var failingEventHandler = Substitute.For(); + var eventHandler = Substitute.For(); + + failingEventHandler.When(x => x.Invoke(Arg.Any())) + .Do(x => throw new Exception()); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var testProvider = new TestProvider(fixture.Create()); + await Api.Instance.SetProvider(testProvider); + + failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + public async Task Client_Level_Event_Handlers_Should_Be_Registered() + { + var fixture = new Fixture(); + var eventHandler = Substitute.For(); + + var myClient = Api.Instance.GetClient(fixture.Create()); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] + public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() + { + var fixture = new Fixture(); + + var failingEventHandler = Substitute.For(); + var eventHandler = Substitute.For(); + + failingEventHandler.When(x => x.Invoke(Arg.Any())) + .Do(x => throw new Exception()); + + var myClient = Api.Instance.GetClient(fixture.Create()); + + myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + + Thread.Sleep(1000); + + failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Provider() + { + var fixture = new Fixture(); + var eventHandler = Substitute.For(); + var clientEventHandler = Substitute.For(); + + var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create()); + var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create()); + + var apiProvider = new TestProvider(fixture.Create()); + var clientProvider = new TestProvider(fixture.Create()); + + // set the default provider on API level, but not specifically to the client + await Api.Instance.SetProvider(apiProvider); + // set the other provider specifically for the client + await Api.Instance.SetProvider(myClientWithBoundProvider.GetMetadata().Name, clientProvider); + + myClientWithNoBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + myClientWithBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, clientEventHandler); + + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name)); + eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name)); + + clientEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name)); + clientEventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")] + public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Named_Provider_Instead_of_Default() + { + var fixture = new Fixture(); + var clientEventHandler = Substitute.For(); + + var client = Api.Instance.GetClient(fixture.Create()); + + var defaultProvider = new TestProvider(fixture.Create()); + var clientProvider = new TestProvider(fixture.Create()); + + // set the default provider + await Api.Instance.SetProvider(defaultProvider); + + client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler); + + defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + + Thread.Sleep(1000); + + // verify that the client received the event from the default provider as there is no named provider registered yet + clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + + // set the other provider specifically for the client + await Api.Instance.SetProvider(client.GetMetadata().Name, clientProvider); + + // now, send another event for the default handler + defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + clientProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + + Thread.Sleep(1000); + + // now the client should have received only the event from the named provider + clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + // for the default provider, the number of received events should stay unchanged + clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering() + { + var fixture = new Fixture(); + var eventHandler = Substitute.For(); + + var myClient = Api.Instance.GetClient(fixture.Create()); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + + // add the event handler after the provider has already transitioned into the ready state + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")] + public async Task Client_Level_Event_Handlers_Should_Be_Removable() + { + var fixture = new Fixture(); + + var eventHandler = Substitute.For(); + + var myClient = Api.Instance.GetClient(fixture.Create()); + + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + + // wait for the first event to be received + Thread.Sleep(1000); + myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); + + // send another event from the provider - this one should not be received + testProvider.SendEvent(ProviderEventTypes.ProviderReady); + + // wait a bit and make sure we only have received the first event, but nothing after removing the event handler + Thread.Sleep(1000); + eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + } +} diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index afcfcd18..3e8b4d3d 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -11,6 +11,11 @@ namespace OpenFeature.Tests { public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { + static async Task EmptyShutdown() + { + await Task.FromResult(0).ConfigureAwait(false); + } + [Fact] [Specification("1.1.1", "The `API`, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.")] public void OpenFeature_Should_Be_Singleton() @@ -74,6 +79,9 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() [Specification("1.6.1", "The API MUST define a mechanism to propagate a shutdown request to active providers.")] public async Task OpenFeature_Should_Support_Shutdown() { + // configure the shutdown method of the event executor to do nothing + // to prevent eventing tests from failing + Api.Instance.EventExecutor.SetShutdownDelegate(EmptyShutdown); var providerA = Substitute.For(); providerA.GetStatus().Returns(ProviderStatus.NotReady); @@ -96,13 +104,13 @@ public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_P var openFeature = Api.Instance; openFeature.SetProvider(new NoOpFeatureProvider()); - openFeature.SetProvider(TestProvider.Name, new TestProvider()); + openFeature.SetProvider(TestProvider.DefaultName, new TestProvider()); var defaultClient = openFeature.GetProviderMetadata(); - var namedClient = openFeature.GetProviderMetadata(TestProvider.Name); + var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); defaultClient.Name.Should().Be(NoOpProvider.NoOpProviderName); - namedClient.Name.Should().Be(TestProvider.Name); + namedClient.Name.Should().Be(TestProvider.DefaultName); } [Fact] @@ -115,7 +123,7 @@ public void OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() var defaultClient = openFeature.GetProviderMetadata(); - defaultClient.Name.Should().Be(TestProvider.Name); + defaultClient.Name.Should().Be(TestProvider.DefaultName); } [Fact] diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index e2bcf5e9..9683e7ef 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; +using OpenFeature.Constant; using OpenFeature.Model; namespace OpenFeature.Tests @@ -36,15 +37,31 @@ public class TestProvider : FeatureProvider { private readonly List _hooks = new List(); - public static string Name => "test-provider"; + public static string DefaultName = "test-provider"; + + public string Name { get; set; } + + private ProviderStatus _status; public void AddHook(Hook hook) => this._hooks.Add(hook); public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList(); + public TestProvider() + { + this._status = ProviderStatus.NotReady; + this.Name = DefaultName; + } + + public TestProvider(string name) + { + this._status = ProviderStatus.NotReady; + this.Name = name; + } + public override Metadata GetMetadata() { - return new Metadata(Name); + return new Metadata(this.Name); } public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, @@ -76,5 +93,27 @@ public override Task> ResolveStructureValue(string flag { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } + + public override ProviderStatus GetStatus() + { + return this._status; + } + + public void SetStatus(ProviderStatus status) + { + this._status = status; + } + + public override Task Initialize(EvaluationContext context) + { + this._status = ProviderStatus.Ready; + this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }); + return base.Initialize(context); + } + + internal void SendEvent(ProviderEventTypes eventType) + { + this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name }); + } } } From c1a189a5cff7106d37f0a45dd5824f18e7ec0cd6 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 16 Jan 2024 14:32:01 -0500 Subject: [PATCH 112/316] chore: Add GitHub Actions logger for CI (#174) Signed-off-by: Austin Drenski --- .github/workflows/ci.yml | 4 ++-- .github/workflows/e2e.yml | 2 +- build/Common.tests.props | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51b3f741..df23b66b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: 7.0.x - name: Run Tests - run: dotnet test test/OpenFeature.Tests/ --configuration Release --logger:"console;verbosity=detailed" + run: dotnet test test/OpenFeature.Tests/ --configuration Release --logger GitHubActions unit-tests-windows: runs-on: windows-latest @@ -43,4 +43,4 @@ jobs: 7.0.x - name: Run Tests - run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger:"console;verbosity=detailed" + run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger GitHubActions diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3b00d994..c13d1d54 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -36,4 +36,4 @@ jobs: cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ - name: Run Tests - run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger:"console;verbosity=detailed" + run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions diff --git a/build/Common.tests.props b/build/Common.tests.props index 7ed10e63..060e749d 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -15,6 +15,10 @@ + + + + + [8.0.0,) [2.0,) [1.0.0,2.0) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 440242da..e2690a7a 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -17,15 +17,13 @@ namespace OpenFeature public sealed class Api : IEventBus { private EvaluationContext _evaluationContext = EvaluationContext.Empty; - private readonly ProviderRepository _repository = new ProviderRepository(); + private EventExecutor _eventExecutor = new EventExecutor(); + private ProviderRepository _repository = new ProviderRepository(); private readonly ConcurrentStack _hooks = new ConcurrentStack(); /// The reader/writer locks are not disposed because the singleton instance should never be disposed. private readonly ReaderWriterLockSlim _evaluationContextLock = new ReaderWriterLockSlim(); - internal readonly EventExecutor EventExecutor = new EventExecutor(); - - /// /// Singleton instance of Api /// @@ -45,7 +43,7 @@ private Api() { } /// Implementation of public async Task SetProvider(FeatureProvider featureProvider) { - this.EventExecutor.RegisterDefaultFeatureProvider(featureProvider); + this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); } @@ -58,7 +56,7 @@ public async Task SetProvider(FeatureProvider featureProvider) /// Implementation of public async Task SetProvider(string clientName, FeatureProvider featureProvider) { - this.EventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); + this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); } @@ -224,20 +222,28 @@ public EvaluationContext GetContext() /// public async Task Shutdown() { - await this._repository.Shutdown().ConfigureAwait(false); - await this.EventExecutor.Shutdown().ConfigureAwait(false); + await using (this._eventExecutor.ConfigureAwait(false)) + await using (this._repository.ConfigureAwait(false)) + { + this._evaluationContext = EvaluationContext.Empty; + this._hooks.Clear(); + + // TODO: make these lazy to avoid extra allocations on the common cleanup path? + this._eventExecutor = new EventExecutor(); + this._repository = new ProviderRepository(); + } } /// public void AddHandler(ProviderEventTypes type, EventHandlerDelegate handler) { - this.EventExecutor.AddApiLevelHandler(type, handler); + this._eventExecutor.AddApiLevelHandler(type, handler); } /// public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) { - this.EventExecutor.RemoveApiLevelHandler(type, handler); + this._eventExecutor.RemoveApiLevelHandler(type, handler); } /// @@ -246,7 +252,13 @@ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) /// The logger to be used public void SetLogger(ILogger logger) { - this.EventExecutor.Logger = logger; + this._eventExecutor.Logger = logger; } + + internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler) + => this._eventExecutor.AddClientHandler(client, eventType, handler); + + internal void RemoveClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler) + => this._eventExecutor.RemoveClientHandler(client, eventType, handler); } } diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 8a6df9a4..7bdfeb6e 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -10,19 +10,13 @@ namespace OpenFeature { - - internal delegate Task ShutdownDelegate(); - - internal class EventExecutor + internal class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); public readonly Channel EventChannel = Channel.CreateBounded(1); - private FeatureProviderReference _defaultProvider; - private readonly Dictionary _namedProviderReferences = new Dictionary(); - private readonly List _activeSubscriptions = new List(); - private readonly SemaphoreSlim _shutdownSemaphore = new SemaphoreSlim(0); - - private ShutdownDelegate _shutdownDelegate; + private FeatureProvider _defaultProvider; + private readonly Dictionary _namedProviderReferences = new Dictionary(); + private readonly List _activeSubscriptions = new List(); private readonly Dictionary> _apiHandlers = new Dictionary>(); private readonly Dictionary>> _clientHandlers = new Dictionary>>(); @@ -32,11 +26,12 @@ internal class EventExecutor public EventExecutor() { this.Logger = new Logger(new NullLoggerFactory()); - this._shutdownDelegate = this.SignalShutdownAsync; var eventProcessing = new Thread(this.ProcessEventAsync); eventProcessing.Start(); } + public ValueTask DisposeAsync() => new(this.Shutdown()); + internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) { lock (this._lockObj) @@ -114,7 +109,7 @@ internal void RegisterDefaultFeatureProvider(FeatureProvider provider) { var oldProvider = this._defaultProvider; - this._defaultProvider = new FeatureProviderReference(provider); + this._defaultProvider = provider; this.StartListeningAndShutdownOld(this._defaultProvider, oldProvider); } @@ -128,8 +123,8 @@ internal void RegisterClientFeatureProvider(string client, FeatureProvider provi } lock (this._lockObj) { - var newProvider = new FeatureProviderReference(provider); - FeatureProviderReference oldProvider = null; + var newProvider = provider; + FeatureProvider oldProvider = null; if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider)) { oldProvider = foundOldProvider; @@ -141,7 +136,7 @@ internal void RegisterClientFeatureProvider(string client, FeatureProvider provi } } - private void StartListeningAndShutdownOld(FeatureProviderReference newProvider, FeatureProviderReference 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)) @@ -154,15 +149,11 @@ private void StartListeningAndShutdownOld(FeatureProviderReference newProvider, if (oldProvider != null && !this.IsProviderBound(oldProvider)) { this._activeSubscriptions.Remove(oldProvider); - var channel = oldProvider.Provider.GetEventChannel(); - if (channel != null) - { - channel.Writer.WriteAsync(new ShutdownSignal()); - } + oldProvider.GetEventChannel()?.Writer.Complete(); } } - private bool IsProviderBound(FeatureProviderReference provider) + private bool IsProviderBound(FeatureProvider provider) { if (this._defaultProvider == provider) { @@ -178,18 +169,18 @@ private bool IsProviderBound(FeatureProviderReference provider) return false; } - private bool IsProviderActive(FeatureProviderReference providerRef) + private bool IsProviderActive(FeatureProvider providerRef) { return this._activeSubscriptions.Contains(providerRef); } - private void EmitOnRegistration(FeatureProviderReference provider, ProviderEventTypes eventType, EventHandlerDelegate handler) + private void EmitOnRegistration(FeatureProvider provider, ProviderEventTypes eventType, EventHandlerDelegate handler) { if (provider == null) { return; } - var status = provider.Provider.GetStatus(); + var status = provider.GetStatus(); var message = ""; if (status == ProviderStatus.Ready && eventType == ProviderEventTypes.ProviderReady) @@ -211,7 +202,7 @@ private void EmitOnRegistration(FeatureProviderReference provider, ProviderEvent { handler.Invoke(new ProviderEventPayload { - ProviderName = provider.Provider?.GetMetadata()?.Name, + ProviderName = provider.GetMetadata()?.Name, Type = eventType, Message = message }); @@ -225,23 +216,22 @@ private void EmitOnRegistration(FeatureProviderReference provider, ProviderEvent private async void ProcessFeatureProviderEventsAsync(object providerRef) { - while (true) + var typedProviderRef = (FeatureProvider)providerRef; + if (typedProviderRef.GetEventChannel() is not { Reader: { } reader }) { - var typedProviderRef = (FeatureProviderReference)providerRef; - if (typedProviderRef.Provider.GetEventChannel() == null) - { - return; - } - var item = await typedProviderRef.Provider.GetEventChannel().Reader.ReadAsync().ConfigureAwait(false); + return; + } + + while (await reader.WaitToReadAsync().ConfigureAwait(false)) + { + if (!reader.TryRead(out var item)) + continue; switch (item) { case ProviderEventPayload eventPayload: await this.EventChannel.Writer.WriteAsync(new Event { Provider = typedProviderRef, EventPayload = eventPayload }).ConfigureAwait(false); break; - case ShutdownSignal _: - typedProviderRef.ShutdownSemaphore.Release(); - return; } } } @@ -249,9 +239,10 @@ private async void ProcessFeatureProviderEventsAsync(object providerRef) // Method to process events private async void ProcessEventAsync() { - while (true) + while (await this.EventChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) { - var item = await this.EventChannel.Reader.ReadAsync().ConfigureAwait(false); + if (!this.EventChannel.Reader.TryRead(out var item)) + continue; switch (item) { @@ -307,9 +298,6 @@ private async void ProcessEventAsync() } } break; - case ShutdownSignal _: - this._shutdownSemaphore.Release(); - return; } } @@ -329,43 +317,15 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) public async Task Shutdown() { - await this._shutdownDelegate().ConfigureAwait(false); - } + this.EventChannel.Writer.Complete(); - internal void SetShutdownDelegate(ShutdownDelegate del) - { - this._shutdownDelegate = del; - } - - // Method to signal shutdown - private async Task SignalShutdownAsync() - { - // Enqueue a shutdown signal - await this.EventChannel.Writer.WriteAsync(new ShutdownSignal()).ConfigureAwait(false); - - // Wait for the processing loop to acknowledge the shutdown - await this._shutdownSemaphore.WaitAsync().ConfigureAwait(false); - } - } - - internal class ShutdownSignal - { - } - - internal class FeatureProviderReference - { - internal readonly SemaphoreSlim ShutdownSemaphore = new SemaphoreSlim(0); - internal FeatureProvider Provider { get; } - - public FeatureProviderReference(FeatureProvider provider) - { - this.Provider = provider; + await this.EventChannel.Reader.Completion.ConfigureAwait(false); } } internal class Event { - internal FeatureProviderReference Provider { get; set; } + internal FeatureProvider Provider { get; set; } internal ProviderEventPayload EventPayload { get; set; } } } diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index 5429f43b..a6a5828f 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -7,6 +7,8 @@ + + diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index d979dae1..56fc518f 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -96,13 +96,13 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// public void AddHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) { - Api.Instance.EventExecutor.AddClientHandler(this._metadata.Name, eventType, handler); + Api.Instance.AddClientHandler(this._metadata.Name, eventType, handler); } /// public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) { - Api.Instance.EventExecutor.RemoveClientHandler(this._metadata.Name, type, handler); + Api.Instance.RemoveClientHandler(this._metadata.Name, type, handler); } /// diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index ab2bdb30..f95d805c 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -12,7 +12,7 @@ namespace OpenFeature /// /// This class manages the collection of providers, both default and named, contained by the API. /// - internal sealed class ProviderRepository + internal sealed class ProviderRepository : IAsyncDisposable { private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); @@ -31,6 +31,14 @@ internal sealed class ProviderRepository /// of that provider under different names.. private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); + public async ValueTask DisposeAsync() + { + using (this._providersLock) + { + await this.Shutdown().ConfigureAwait(false); + } + } + /// /// Set the default provider /// diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 3e8b4d3d..28ba9f42 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -11,11 +11,6 @@ namespace OpenFeature.Tests { public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { - static async Task EmptyShutdown() - { - await Task.FromResult(0).ConfigureAwait(false); - } - [Fact] [Specification("1.1.1", "The `API`, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.")] public void OpenFeature_Should_Be_Singleton() @@ -79,9 +74,6 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() [Specification("1.6.1", "The API MUST define a mechanism to propagate a shutdown request to active providers.")] public async Task OpenFeature_Should_Support_Shutdown() { - // configure the shutdown method of the event executor to do nothing - // to prevent eventing tests from failing - Api.Instance.EventExecutor.SetShutdownDelegate(EmptyShutdown); var providerA = Substitute.For(); providerA.GetStatus().Returns(ProviderStatus.NotReady); From f2b9b03eda5f6d6b4a738f761702cd9d9a105e76 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 19 Jan 2024 16:32:01 -0500 Subject: [PATCH 121/316] chore: remove test sleeps, fix flaky test (#194) Signed-off-by: Todd Baert Co-authored-by: Austin Drenski --- .../OpenFeatureEventTests.cs | 83 ++++++++++--------- test/OpenFeature.Tests/TestUtils.cs | 54 ++++++++++++ test/OpenFeature.Tests/TestUtilsTest.cs | 23 +++++ 3 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 test/OpenFeature.Tests/TestUtils.cs create mode 100644 test/OpenFeature.Tests/TestUtilsTest.cs diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 8c183e63..42558e88 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -76,34 +76,33 @@ public async Task API_Level_Event_Handlers_Should_Be_Registered() testProvider.SendEvent(ProviderEventTypes.ProviderError); testProvider.SendEvent(ProviderEventTypes.ProviderStale); - Thread.Sleep(1000); - eventHandler + await Utils.AssertUntilAsync(_ => eventHandler .Received() .Invoke( Arg.Is( payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady - )); + ))); - eventHandler + await Utils.AssertUntilAsync(_ => eventHandler .Received() .Invoke( Arg.Is( payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged - )); + ))); - eventHandler + await Utils.AssertUntilAsync(_ => eventHandler .Received() .Invoke( Arg.Is( payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError - )); + ))); - eventHandler + await Utils.AssertUntilAsync(_ => eventHandler .Received() .Invoke( Arg.Is( payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale - )); + ))); } [Fact] @@ -122,13 +121,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - Thread.Sleep(1000); - eventHandler + await Utils.AssertUntilAsync(_ => eventHandler .Received() .Invoke( Arg.Is( payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady - )); + ))); } [Fact] @@ -149,13 +147,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_ Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); - Thread.Sleep(1000); - eventHandler + await Utils.AssertUntilAsync(_ => eventHandler .Received() .Invoke( Arg.Is( payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError - )); + ))); } [Fact] @@ -175,13 +172,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_ Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); - Thread.Sleep(1000); - eventHandler + await Utils.AssertUntilAsync(_ => eventHandler .Received() .Invoke( Arg.Is( payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale - )); + ))); } [Fact] @@ -207,9 +203,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() newTestProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); - Thread.Sleep(1000); - eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)); - eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + await Utils.AssertUntilAsync( + _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)) + ); + await Utils.AssertUntilAsync( + _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); } [Fact] @@ -257,8 +256,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler var testProvider = new TestProvider(fixture.Create()); await Api.Instance.SetProvider(testProvider); - failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + await Utils.AssertUntilAsync( + _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + await Utils.AssertUntilAsync( + _ => eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); } [Fact] @@ -305,10 +308,12 @@ public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Hand var testProvider = new TestProvider(); await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); - Thread.Sleep(1000); - - failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + await Utils.AssertUntilAsync( + _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + await Utils.AssertUntilAsync( + _ => eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); } [Fact] @@ -368,10 +373,10 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); - Thread.Sleep(1000); - // verify that the client received the event from the default provider as there is no named provider registered yet - clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + await Utils.AssertUntilAsync( + _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); // set the other provider specifically for the client await Api.Instance.SetProvider(client.GetMetadata().Name, clientProvider); @@ -380,12 +385,14 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); clientProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); - Thread.Sleep(1000); - // now the client should have received only the event from the named provider - clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + await Utils.AssertUntilAsync( + _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); // for the default provider, the number of received events should stay unchanged - clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + await Utils.AssertUntilAsync( + _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); } [Fact] @@ -431,15 +438,15 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); // wait for the first event to be received - Thread.Sleep(1000); - myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); + await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); // send another event from the provider - this one should not be received testProvider.SendEvent(ProviderEventTypes.ProviderReady); // wait a bit and make sure we only have received the first event, but nothing after removing the event handler - Thread.Sleep(1000); - eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + await Utils.AssertUntilAsync( + _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); } } } diff --git a/test/OpenFeature.Tests/TestUtils.cs b/test/OpenFeature.Tests/TestUtils.cs new file mode 100644 index 00000000..c7cfb347 --- /dev/null +++ b/test/OpenFeature.Tests/TestUtils.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +internal class Utils +{ + /// + /// Repeatedly runs the supplied assertion until it doesn't throw, or the timeout is reached. + /// + /// Function which makes an assertion + /// Timeout in millis (defaults to 1000) + /// Poll interval (defaults to 100 + /// + public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, int pollIntervalMillis = 100) + { + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(default(CancellationToken))) + { + + cts.CancelAfter(timeoutMillis); + + var exceptions = new List(); + var message = "AssertUntilAsync timeout reached."; + + while (!cts.IsCancellationRequested) + { + try + { + assertionFunc(cts.Token); + return; + } + catch (TaskCanceledException) when (cts.IsCancellationRequested) + { + throw new AggregateException(message, exceptions); + } + catch (Exception e) + { + exceptions.Add(e); + } + + try + { + await Task.Delay(pollIntervalMillis, cts.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + throw new AggregateException(message, exceptions); + } + } + throw new AggregateException(message, exceptions); + } + } +} diff --git a/test/OpenFeature.Tests/TestUtilsTest.cs b/test/OpenFeature.Tests/TestUtilsTest.cs new file mode 100644 index 00000000..141194b3 --- /dev/null +++ b/test/OpenFeature.Tests/TestUtilsTest.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Tests +{ + public class TestUtilsTest + { + [Fact] + public async void Should_Fail_If_Assertion_Fails() + { + await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)).ConfigureAwait(false); + } + + [Fact] + public async void Should_Pass_If_Assertion_Fails() + { + await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))).ConfigureAwait(false); + } + } +} From f47cf07420cdcb6bc74b0455898b7b17a144daf3 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Fri, 19 Jan 2024 17:07:27 -0500 Subject: [PATCH 122/316] chore: Fix props to support more than one project (#177) Signed-off-by: Austin Drenski --- .github/workflows/release.yml | 2 +- build/Common.prod.props | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb0a0f5a..04f4aedc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: - name: Pack if: ${{ steps.release.outputs.releases_created }} run: | - dotnet pack OpenFeature.proj --configuration Release --no-build -p:PackageID=OpenFeature + dotnet pack OpenFeature.proj --configuration Release --no-build - name: Publish to Nuget if: ${{ steps.release.outputs.releases_created }} diff --git a/build/Common.prod.props b/build/Common.prod.props index 49e454c5..63a52d44 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -11,7 +11,6 @@ https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. Feature;OpenFeature;Flags; - OpenFeature openfeature-icon.png https://openfeature.dev Apache-2.0 From 26cd5cdd613577c53ae79b889d1cf2d89262236f Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Fri, 19 Jan 2024 18:25:38 -0500 Subject: [PATCH 123/316] chore: Add support for GitHub Packages (#173) Signed-off-by: Austin Drenski --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ build/Common.prod.props | 12 ++++------- build/Common.props | 4 ++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9ff3649..ca5f5083 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,3 +44,46 @@ jobs: - name: Run Tests run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger GitHubActions + + packaging: + needs: + - unit-tests-linux + - unit-tests-windows + + permissions: + contents: read + packages: write + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + + - name: Restore + run: dotnet restore + + - name: Pack NuGet packages (CI versions) + if: startsWith(github.ref, 'refs/heads/') + run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + + - name: Pack NuGet packages (PR versions) + if: startsWith(github.ref, 'refs/pull/') + run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + + - name: Publish NuGet packages (base) + if: github.event.pull_request.head.repo.fork == false + run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json + + - name: Publish NuGet packages (fork) + if: github.event.pull_request.head.repo.fork == true + uses: actions/upload-artifact@v4.2.0 + with: + name: nupkgs + path: src/**/*.nupkg diff --git a/build/Common.prod.props b/build/Common.prod.props index 63a52d44..4d073ecf 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -2,7 +2,10 @@ + true + true true + true @@ -16,7 +19,7 @@ Apache-2.0 OpenFeature Authors true - $(VersionNumber) + $(VersionNumber) $(VersionNumber) $(VersionNumber) @@ -32,11 +35,4 @@ snupkg - - - - - - true - diff --git a/build/Common.props b/build/Common.props index c33ed3ec..43424f59 100644 --- a/build/Common.props +++ b/build/Common.props @@ -28,4 +28,8 @@ + + + + From 2919c2f4f2a4629fccd1a50b1885375006445b96 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 22 Jan 2024 20:53:13 -0500 Subject: [PATCH 124/316] chore: revert breaking setProvider (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Todd Baert Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Austin Drenski --- src/OpenFeature/Api.cs | 30 +++++++++- .../Steps/EvaluationStepDefinitions.cs | 2 +- .../ClearOpenFeatureInstanceFixture.cs | 2 +- .../OpenFeatureClientTests.cs | 22 +++---- .../OpenFeatureEventTests.cs | 60 +++++++++++++------ .../OpenFeature.Tests/OpenFeatureHookTests.cs | 18 +++--- test/OpenFeature.Tests/OpenFeatureTests.cs | 46 +++++++------- .../ProviderRepositoryTests.cs | 16 ++--- 8 files changed, 123 insertions(+), 73 deletions(-) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index e2690a7a..af302e7e 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -36,17 +37,40 @@ static Api() { } private Api() { } /// - /// Sets the feature provider. In order to wait for the provider to be set, and initialization to complete, + /// Sets the default feature provider to given clientName without awaiting its initialization. + /// + /// The provider cannot be set to null. Attempting to set the provider to null has no effect. + /// Implementation of + [Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")] + public void SetProvider(FeatureProvider featureProvider) + { + this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); + _ = this._repository.SetProvider(featureProvider, this.GetContext()); + } + + /// + /// 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. /// Implementation of - public async Task SetProvider(FeatureProvider featureProvider) + public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); } + /// + /// Sets the feature provider to given clientName without awaiting its initialization. + /// + /// Name of client + /// Implementation of + [Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")] + public void SetProvider(string clientName, FeatureProvider featureProvider) + { + this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); + _ = this._repository.SetProvider(clientName, featureProvider, this.GetContext()); + } /// /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and @@ -54,7 +78,7 @@ public async Task SetProvider(FeatureProvider featureProvider) /// /// Name of client /// Implementation of - public async Task SetProvider(string clientName, FeatureProvider featureProvider) + public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider) { this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 4847bfb2..d2cd483d 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -42,7 +42,7 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) { _scenarioContext = scenarioContext; var flagdProvider = new FlagdProvider(); - Api.Instance.SetProvider(flagdProvider).Wait(); + Api.Instance.SetProviderAsync(flagdProvider).Wait(); client = Api.Instance.GetClient(); } diff --git a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs index a70921f7..5f31c71a 100644 --- a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs +++ b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs @@ -7,7 +7,7 @@ public ClearOpenFeatureInstanceFixture() { Api.Instance.SetContext(null); Api.Instance.ClearHooks(); - Api.Instance.SetProvider(new NoOpFeatureProvider()).Wait(); + Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).Wait(); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 30bee168..86c61f83 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -75,7 +75,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - await Api.Instance.SetProvider(new NoOpFeatureProvider()); + await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); @@ -121,7 +121,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var defaultStructureValue = fixture.Create(); var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - await Api.Instance.SetProvider(new NoOpFeatureProvider()); + await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); var client = Api.Instance.GetClient(clientName, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); @@ -172,7 +172,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedFeatureProvider.GetMetadata().Returns(new Metadata(fixture.Create())); mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(mockedFeatureProvider); + await Api.Instance.SetProviderAsync(mockedFeatureProvider); var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger); var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); @@ -202,7 +202,7 @@ public async Task Should_Resolve_BooleanValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetBooleanValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -224,7 +224,7 @@ public async Task Should_Resolve_StringValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetStringValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -246,7 +246,7 @@ public async Task Should_Resolve_IntegerValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -268,7 +268,7 @@ public async Task Should_Resolve_DoubleValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -290,7 +290,7 @@ public async Task Should_Resolve_StructureValue() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); @@ -313,7 +313,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); @@ -338,7 +338,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProvider(featureProviderMock); + await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); var response = await client.GetObjectDetails(flagName, defaultValue); @@ -351,7 +351,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() [Fact] public async Task Should_Use_No_Op_When_Provider_Is_Null() { - await Api.Instance.SetProvider(null); + await Api.Instance.SetProviderAsync(null); var client = new FeatureClient("test", "test"); (await client.GetIntegerValue("some-key", 12)).Should().Be(12); } diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 42558e88..525241fc 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -70,7 +70,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Registered() Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); testProvider.SendEvent(ProviderEventTypes.ProviderError); @@ -117,7 +117,33 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ var eventHandler = Substitute.For(); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady + ))); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Ready_Sync() + { + var eventHandler = Substitute.For(); + + var testProvider = new TestProvider(); +#pragma warning disable CS0618 // Type or member is obsolete + Api.Instance.SetProvider(testProvider); +#pragma warning restore CS0618 // Type or member is obsolete Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -141,7 +167,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_ var eventHandler = Substitute.For(); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); testProvider.SetStatus(ProviderStatus.Error); @@ -166,7 +192,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_ var eventHandler = Substitute.For(); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); testProvider.SetStatus(ProviderStatus.Stale); @@ -194,12 +220,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); var newTestProvider = new TestProvider(); - await Api.Instance.SetProvider(newTestProvider); + await Api.Instance.SetProviderAsync(newTestProvider); newTestProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); @@ -223,13 +249,13 @@ public async Task API_Level_Event_Handlers_Should_Be_Removable() Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); Thread.Sleep(1000); Api.Instance.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); var newTestProvider = new TestProvider(); - await Api.Instance.SetProvider(newTestProvider); + await Api.Instance.SetProviderAsync(newTestProvider); eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); } @@ -254,7 +280,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(fixture.Create()); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); await Utils.AssertUntilAsync( _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) @@ -277,7 +303,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered() var myClient = Api.Instance.GetClient(fixture.Create()); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -306,7 +332,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Hand myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); await Utils.AssertUntilAsync( _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) @@ -335,9 +361,9 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Pr var clientProvider = new TestProvider(fixture.Create()); // set the default provider on API level, but not specifically to the client - await Api.Instance.SetProvider(apiProvider); + await Api.Instance.SetProviderAsync(apiProvider); // set the other provider specifically for the client - await Api.Instance.SetProvider(myClientWithBoundProvider.GetMetadata().Name, clientProvider); + await Api.Instance.SetProviderAsync(myClientWithBoundProvider.GetMetadata().Name, clientProvider); myClientWithNoBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); myClientWithBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, clientEventHandler); @@ -367,7 +393,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name var clientProvider = new TestProvider(fixture.Create()); // set the default provider - await Api.Instance.SetProvider(defaultProvider); + await Api.Instance.SetProviderAsync(defaultProvider); client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler); @@ -379,7 +405,7 @@ await Utils.AssertUntilAsync( ); // set the other provider specifically for the client - await Api.Instance.SetProvider(client.GetMetadata().Name, clientProvider); + await Api.Instance.SetProviderAsync(client.GetMetadata().Name, clientProvider); // now, send another event for the default handler defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); @@ -410,7 +436,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_Sta var myClient = Api.Instance.GetClient(fixture.Create()); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); // add the event handler after the provider has already transitioned into the ready state myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -435,7 +461,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); // wait for the first event to be received await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index b3aee4d8..b4cb958c 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -51,7 +51,7 @@ public async Task Hooks_Should_Be_Called_In_Order() var testProvider = new TestProvider(); testProvider.AddHook(providerHook); Api.Instance.AddHooks(apiHook); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); var client = Api.Instance.GetClient(clientName, clientVersion); client.AddHooks(clientHook); @@ -197,7 +197,7 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() provider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); - await Api.Instance.SetProvider(provider); + await Api.Instance.SetProviderAsync(provider); var hook = Substitute.For(); hook.Before(Arg.Any>(), Arg.Any>()).Returns(hookContext); @@ -269,7 +269,7 @@ public async Task Hook_Should_Execute_In_Correct_Order() _ = hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); _ = hook.Finally(Arg.Any>(), Arg.Any>()); - await Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook); @@ -301,7 +301,7 @@ public async Task Register_Hooks_Should_Be_Available_At_All_Levels() var testProvider = new TestProvider(); testProvider.AddHook(hook4); Api.Instance.AddHooks(hook1); - await Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook2); await client.GetBooleanValue("test", false, null, @@ -332,7 +332,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() hook2.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); hook1.Finally(Arg.Any>(), null).Throws(new Exception()); - await Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); client.GetHooks().Count().Should().Be(2); @@ -377,7 +377,7 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() hook2.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); hook1.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); - await Api.Instance.SetProvider(featureProvider1); + await Api.Instance.SetProviderAsync(featureProvider1); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); @@ -414,7 +414,7 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ _ = hook1.Error(Arg.Any>(), Arg.Any(), null); _ = hook2.Error(Arg.Any>(), Arg.Any(), null); - await Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); @@ -459,7 +459,7 @@ public async Task Hook_Hints_May_Be_Optional() hook.Finally(Arg.Any>(), Arg.Any>()) .Returns(Task.CompletedTask); - await Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); await client.GetBooleanValue("test", false, EvaluationContext.Empty, flagOptions); @@ -537,7 +537,7 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() hook.Finally(Arg.Any>(), Arg.Any>()) .Returns(Task.CompletedTask); - await Api.Instance.SetProvider(featureProvider); + await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); var resolvedFlag = await client.GetBooleanValue("test", true, config: flagOptions); diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 28ba9f42..d43bf045 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -28,13 +28,13 @@ public async Task OpenFeature_Should_Initialize_Provider() var providerMockDefault = Substitute.For(); providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProvider(providerMockDefault).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerMockDefault).ConfigureAwait(false); await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); var providerMockNamed = Substitute.For(); providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProvider("the-name", providerMockNamed).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("the-name", providerMockNamed).ConfigureAwait(false); await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); } @@ -46,26 +46,26 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() var providerA = Substitute.For(); providerA.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProvider(providerA).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); await providerA.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProvider(providerB).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerB).ConfigureAwait(false); await providerB.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); await providerA.Received(1).Shutdown().ConfigureAwait(false); var providerC = Substitute.For(); providerC.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProvider("named", providerC).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerC).ConfigureAwait(false); await providerC.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); var providerD = Substitute.For(); providerD.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProvider("named", providerD).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerD).ConfigureAwait(false); await providerD.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); await providerC.Received(1).Shutdown().ConfigureAwait(false); } @@ -80,8 +80,8 @@ public async Task OpenFeature_Should_Support_Shutdown() var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProvider(providerA).ConfigureAwait(false); - await Api.Instance.SetProvider("named", providerB).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerB).ConfigureAwait(false); await Api.Instance.Shutdown().ConfigureAwait(false); @@ -91,12 +91,12 @@ public async Task OpenFeature_Should_Support_Shutdown() [Fact] [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider() + public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider() { var openFeature = Api.Instance; - openFeature.SetProvider(new NoOpFeatureProvider()); - openFeature.SetProvider(TestProvider.DefaultName, new TestProvider()); + await openFeature.SetProviderAsync(new NoOpFeatureProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()).ConfigureAwait(false); var defaultClient = openFeature.GetProviderMetadata(); var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); @@ -107,11 +107,11 @@ public void OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_P [Fact] [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public void OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() + public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() { var openFeature = Api.Instance; - openFeature.SetProvider(new TestProvider()); + await openFeature.SetProviderAsync(new TestProvider()).ConfigureAwait(false); var defaultClient = openFeature.GetProviderMetadata(); @@ -120,26 +120,26 @@ public void OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() [Fact] [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public void OpenFeature_Should_Assign_Provider_To_Existing_Client() + public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() { const string name = "new-client"; var openFeature = Api.Instance; - openFeature.SetProvider(name, new TestProvider()); - openFeature.SetProvider(name, new NoOpFeatureProvider()); + await openFeature.SetProviderAsync(name, new TestProvider()).ConfigureAwait(true); + await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()).ConfigureAwait(true); openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); } [Fact] [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public void OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance() + public async Task OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance() { var openFeature = Api.Instance; var provider = new TestProvider(); - openFeature.SetProvider("a", provider); - openFeature.SetProvider("b", provider); + await openFeature.SetProviderAsync("a", provider).ConfigureAwait(true); + await openFeature.SetProviderAsync("b", provider).ConfigureAwait(true); var clientA = openFeature.GetProvider("a"); var clientB = openFeature.GetProvider("b"); @@ -180,7 +180,7 @@ public void OpenFeature_Should_Add_Hooks() [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] public void OpenFeature_Should_Get_Metadata() { - Api.Instance.SetProvider(new NoOpFeatureProvider()).Wait(); + Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).Wait(); var openFeature = Api.Instance; var metadata = openFeature.GetProviderMetadata(); @@ -226,12 +226,12 @@ public void Should_Always_Have_Provider() } [Fact] - public void OpenFeature_Should_Allow_Multiple_Client_Mapping() + public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() { var openFeature = Api.Instance; - openFeature.SetProvider("client1", new TestProvider()); - openFeature.SetProvider("client2", new NoOpFeatureProvider()); + await openFeature.SetProviderAsync("client1", new TestProvider()).ConfigureAwait(true); + await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()).ConfigureAwait(true); var client1 = openFeature.GetClient("client1"); var client2 = openFeature.GetClient("client2"); diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 6d2ff310..62cbe9d6 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -16,24 +16,24 @@ namespace OpenFeature.Tests public class ProviderRepositoryTests { [Fact] - public void Default_Provider_Is_Set_Without_Await() + public async Task Default_Provider_Is_Set_Without_Await() { var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - repository.SetProvider(provider, context); + await repository.SetProvider(provider, context); Assert.Equal(provider, repository.GetProvider()); } [Fact] - public void AfterSet_Is_Invoked_For_Setting_Default_Provider() + public async void AfterSet_Is_Invoked_For_Setting_Default_Provider() { var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - repository.SetProvider(provider, context, afterSet: (theProvider) => + await repository.SetProvider(provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -182,24 +182,24 @@ await repository.SetProvider(provider2, context, afterError: (provider, ex) => } [Fact] - public void Named_Provider_Provider_Is_Set_Without_Await() + public async Task Named_Provider_Provider_Is_Set_Without_Await() { var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - repository.SetProvider("the-name", provider, context); + await repository.SetProvider("the-name", provider, context); Assert.Equal(provider, repository.GetProvider("the-name")); } [Fact] - public void AfterSet_Is_Invoked_For_Setting_Named_Provider() + public async Task AfterSet_Is_Invoked_For_Setting_Named_Provider() { var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - repository.SetProvider("the-name", provider, context, afterSet: (theProvider) => + await repository.SetProvider("the-name", provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); From 89d50bcb88069655fb69236bc390c499f8b0c1d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:08:20 -0500 Subject: [PATCH 125/316] chore(main): release 1.4.0 (#154) Signed-off-by: Todd Baert Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Todd Baert --- .release-please-manifest.json | 4 +- CHANGELOG.md | 51 +++ README.md | 684 +++++++++++++++++----------------- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 397 insertions(+), 346 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0e5b256d..4c313f93 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.1" -} \ No newline at end of file + ".": "1.4.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eae4599..7f276ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## [1.4.0](https://github.com/open-feature/dotnet-sdk/compare/v1.3.1...v1.4.0) (2024-01-23) + + +### πŸ› Bug Fixes + +* Fix ArgumentOutOfRangeException for empty hooks ([#187](https://github.com/open-feature/dotnet-sdk/issues/187)) ([950775b](https://github.com/open-feature/dotnet-sdk/commit/950775b65093e22ce209c947bf9da71b17ad7387)) +* More robust shutdown/cleanup/reset ([#188](https://github.com/open-feature/dotnet-sdk/issues/188)) ([a790f78](https://github.com/open-feature/dotnet-sdk/commit/a790f78c32b8500ce27d04d906c98d1de2afd2b4)) +* Remove upper-bound version constraint from SCI ([#171](https://github.com/open-feature/dotnet-sdk/issues/171)) ([8f8b661](https://github.com/open-feature/dotnet-sdk/commit/8f8b661f1cac6a4f1c51eb513999372d30a4f726)) + + +### ✨ New Features + +* Add dx to catch ConfigureAwait(false) ([#152](https://github.com/open-feature/dotnet-sdk/issues/152)) ([9c42d4a](https://github.com/open-feature/dotnet-sdk/commit/9c42d4afa9139094e0316bbe1306ae4856b7d013)) +* add support for eventing ([#166](https://github.com/open-feature/dotnet-sdk/issues/166)) ([f5fc1dd](https://github.com/open-feature/dotnet-sdk/commit/f5fc1ddadc11f712ae0893cde815e7a1c6fe2c1b)) +* Add support for provider shutdown and status. ([#158](https://github.com/open-feature/dotnet-sdk/issues/158)) ([24c3441](https://github.com/open-feature/dotnet-sdk/commit/24c344163423973b54a06b73648ba45b944589ee)) + + +### 🧹 Chore + +* Add GitHub Actions logger for CI ([#174](https://github.com/open-feature/dotnet-sdk/issues/174)) ([c1a189a](https://github.com/open-feature/dotnet-sdk/commit/c1a189a5cff7106d37f0a45dd5824f18e7ec0cd6)) +* add placeholder eventing and shutdown sections ([#156](https://github.com/open-feature/dotnet-sdk/issues/156)) ([5dfea29](https://github.com/open-feature/dotnet-sdk/commit/5dfea29bb3d01f6c8640de321c4fde52f283a1c0)) +* Add support for GitHub Packages ([#173](https://github.com/open-feature/dotnet-sdk/issues/173)) ([26cd5cd](https://github.com/open-feature/dotnet-sdk/commit/26cd5cdd613577c53ae79b889d1cf2d89262236f)) +* Adding sealed keyword to classes ([#191](https://github.com/open-feature/dotnet-sdk/issues/191)) ([1a14f6c](https://github.com/open-feature/dotnet-sdk/commit/1a14f6cd6c8988756a2cf2da1137a739e8d960f8)) +* **deps:** update actions/checkout action to v4 ([#144](https://github.com/open-feature/dotnet-sdk/issues/144)) ([90d9d02](https://github.com/open-feature/dotnet-sdk/commit/90d9d021b227fba626bb99454cb7c0f7fef2d8d8)) +* **deps:** update actions/setup-dotnet action to v4 ([#162](https://github.com/open-feature/dotnet-sdk/issues/162)) ([0b0bb10](https://github.com/open-feature/dotnet-sdk/commit/0b0bb10419f836d9cc276fe8ac3c71c9214420ef)) +* **deps:** update dependency dotnet-sdk to v7.0.404 ([#148](https://github.com/open-feature/dotnet-sdk/issues/148)) ([e8ca1da](https://github.com/open-feature/dotnet-sdk/commit/e8ca1da9ed63df9685ec49a9569e0ec99ba0b3b9)) +* **deps:** update github/codeql-action action to v3 ([#163](https://github.com/open-feature/dotnet-sdk/issues/163)) ([c85e93e](https://github.com/open-feature/dotnet-sdk/commit/c85e93e9c9a97083660f9062c38dcbf6d64a3ad6)) +* fix alt text for NuGet on the readme ([2cbdba8](https://github.com/open-feature/dotnet-sdk/commit/2cbdba80d836f8b7850e8dc5f1f1790ef2ed1aca)) +* Fix FieldCanBeMadeReadOnly ([#183](https://github.com/open-feature/dotnet-sdk/issues/183)) ([18a092a](https://github.com/open-feature/dotnet-sdk/commit/18a092afcab1b06c25f3b825a6130d22226790fc)) +* Fix props to support more than one project ([#177](https://github.com/open-feature/dotnet-sdk/issues/177)) ([f47cf07](https://github.com/open-feature/dotnet-sdk/commit/f47cf07420cdcb6bc74b0455898b7b17a144daf3)) +* minor formatting cleanup ([#168](https://github.com/open-feature/dotnet-sdk/issues/168)) ([d0c25af](https://github.com/open-feature/dotnet-sdk/commit/d0c25af7df5176d10088c148eac35b0034536e04)) +* Reduce dependency on MEL -> MELA ([#176](https://github.com/open-feature/dotnet-sdk/issues/176)) ([a6062fe](https://github.com/open-feature/dotnet-sdk/commit/a6062fe2b9f0d83490c7ce900e837863521f5f55)) +* remove duplicate eventing section in readme ([1efe09d](https://github.com/open-feature/dotnet-sdk/commit/1efe09da3948d5dfd7fd9f1c7a040fc5c2cbe833)) +* remove test sleeps, fix flaky test ([#194](https://github.com/open-feature/dotnet-sdk/issues/194)) ([f2b9b03](https://github.com/open-feature/dotnet-sdk/commit/f2b9b03eda5f6d6b4a738f761702cd9d9a105e76)) +* revert breaking setProvider ([#190](https://github.com/open-feature/dotnet-sdk/issues/190)) ([2919c2f](https://github.com/open-feature/dotnet-sdk/commit/2919c2f4f2a4629fccd1a50b1885375006445b96)) +* update spec release link ([a2f70eb](https://github.com/open-feature/dotnet-sdk/commit/a2f70ebd68357156f9045fc6e94845a53ffd204a)) +* updated readme for inclusion in the docs ([6516866](https://github.com/open-feature/dotnet-sdk/commit/6516866ec7601a7adaa4dc6b517c9287dec54fca)) + + +### πŸ“š Documentation + +* Add README.md to the nuget package ([#164](https://github.com/open-feature/dotnet-sdk/issues/164)) ([b6b0ee2](https://github.com/open-feature/dotnet-sdk/commit/b6b0ee2b61a9b0b973b913b53887badfa0c5a3de)) +* fixed the contrib url on the readme ([9d8939e](https://github.com/open-feature/dotnet-sdk/commit/9d8939ef57a3be4ee220bd21f36b166887b2c30b)) +* remove duplicate a tag from readme ([2687cf0](https://github.com/open-feature/dotnet-sdk/commit/2687cf0663e20aa2dd113569cbf177833639cbbd)) +* update README.md ([#155](https://github.com/open-feature/dotnet-sdk/issues/155)) ([b62e21f](https://github.com/open-feature/dotnet-sdk/commit/b62e21f76964e7f6f7456f720814de0997232d71)) + + +### πŸ”„ Refactoring + +* Add TFMs for net{6,7,8}.0 ([#172](https://github.com/open-feature/dotnet-sdk/issues/172)) ([cf2baa8](https://github.com/open-feature/dotnet-sdk/commit/cf2baa8a6b4328f1aa346bbea91160aa2e5f3a8d)) + ## [1.3.1](https://github.com/open-feature/dotnet-sdk/compare/v1.3.0...v1.3.1) (2023-09-19) diff --git a/README.md b/README.md index 33cf5c5d..a2db7e76 100644 --- a/README.md +++ b/README.md @@ -1,342 +1,342 @@ - - -

- - - OpenFeature Logo - -

- -

OpenFeature .NET SDK

- - - -

- - Specification - - - - - Release - - -
- - Slack - - - Codecov - - - NuGet - - - CII Best Practices - - -

- - -[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. - - - -## πŸš€ Quick start - -### Requirements - -- .NET 6+ -- .NET Core 6+ -- .NET Framework 4.6.2+ - -Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 - -### Install - -Use the following to initialize your project: - -```sh -dotnet new console -``` - -and install OpenFeature: - -```sh -dotnet add package OpenFeature -``` - -### Usage - -```csharp -public async Task Example() -{ - // Register your feature flag provider - await Api.Instance.SetProvider(new InMemoryProvider()); - - // Create a new client - FeatureClient client = Api.Instance.GetClient(); - - // Evaluate your feature flag - bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); - - if ( v2Enabled ) - { - //Do some work - } -} -``` - -## 🌟 Features - -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| βœ… | [Logging](#logging) | Integrate with popular logging packages. | -| βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | - -Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ - -### Providers - -[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. -Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). - -If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. - -Once you've added a provider as a dependency, it can be registered with OpenFeature like this: - -```csharp -await Api.Instance.SetProvider(new MyProvider()); -``` - -In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more detail below. - -### Targeting - -Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. -In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). -If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). - -```csharp -// set a value to the global context -EvaluationContextBuilder builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext apiCtx = builder.Build(); -Api.Instance.SetContext(apiCtx); - -// set a value to the client context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext clientCtx = builder.Build(); -var client = Api.Instance.GetClient(); -client.SetContext(clientCtx); - -// set a value to the invocation context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext reqCtx = builder.Build(); - -bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); - -``` - -### Hooks - -[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/). -If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. - -Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. - -```csharp -// add a hook globally, to run on all evaluations -Api.Instance.AddHooks(new ExampleGlobalHook()); - -// add a hook on this client, to run on all evaluations made by this client -var client = Api.Instance.GetClient(); -client.AddHooks(new ExampleClientHook()); - -// add a hook for this evaluation only -var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); -``` - -### Logging - -The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. - -### Named clients - -Clients can be given a name. -A name is a logical identifier that can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. - -```csharp -// registering the default provider -await Api.Instance.SetProvider(new LocalProvider()); - -// registering a named provider -await Api.Instance.SetProvider("clientForCache", new CachedProvider()); - -// a client backed by default provider -FeatureClient clientDefault = Api.Instance.GetClient(); - -// a client backed by CachedProvider -FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); - -``` - -### Eventing - -Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, -provider readiness, or error conditions. -Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. -Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. - -Please refer to the documentation of the provider you're using to see what events are supported. - -Example usage of an Event handler: - -```csharp -public static void EventHandler(ProviderEventPayload eventDetails) -{ - Console.WriteLine(eventDetails.Type); -} -``` - -```csharp -EventHandlerDelegate callback = EventHandler; -// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event -Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -It is also possible to register an event handler for a specific client, as in the following example: - -```csharp -EventHandlerDelegate callback = EventHandler; - -var myClient = Api.Instance.GetClient("my-client"); - -var provider = new ExampleProvider(); -await Api.Instance.SetProvider(myClient.GetMetadata().Name, provider); - -myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -### Shutdown - -The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. - -```csharp -// Shut down all providers -await Api.Instance.Shutdown(); -``` - -## Extending - -### Develop a provider - -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. - -```csharp -public class MyProvider : FeatureProvider -{ - public override Metadata GetMetadata() - { - return new Metadata("My Provider"); - } - - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) - { - // resolve a boolean flag value - } - - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) - { - // resolve a double flag value - } - - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) - { - // resolve an int flag value - } - - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) - { - // resolve a string flag value - } - - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) - { - // resolve an object flag value - } -} -``` - -### Develop a hook - -To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -Implement your own hook by conforming to the `Hook interface`. -To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. - -```csharp -public class MyHook : Hook -{ - public Task Before(HookContext context, - IReadOnlyDictionary hints = null) - { - // code to run before flag evaluation - } - - public virtual Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) - { - // code to run after successful flag evaluation - } - - public virtual Task Error(HookContext context, Exception error, - IReadOnlyDictionary hints = null) - { - // code to run if there's an error during before hooks or during flag evaluation - } - - public virtual Task Finally(HookContext context, IReadOnlyDictionary hints = null) - { - // code to run after all other stages, regardless of success/failure - } -} -``` - -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! - - -## ⭐️ Support the project - -- Give this repo a ⭐️! -- Follow us on social media: - - Twitter: [@openfeature](https://twitter.com/openfeature) - - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) -- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more information, check out our [community page](https://openfeature.dev/community/) - -## 🀝 Contributing - -Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. - -### Thanks to everyone who has already contributed - - - - - -Made with [contrib.rocks](https://contrib.rocks). - + + +

+ + + OpenFeature Logo + +

+ +

OpenFeature .NET SDK

+ + + +

+ + Specification + + + + + Release + + +
+ + Slack + + + Codecov + + + NuGet + + + CII Best Practices + + +

+ + +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. + + + +## πŸš€ Quick start + +### Requirements + +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ + +Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 + +### Install + +Use the following to initialize your project: + +```sh +dotnet new console +``` + +and install OpenFeature: + +```sh +dotnet add package OpenFeature +``` + +### Usage + +```csharp +public async Task Example() +{ + // Register your feature flag provider + await Api.Instance.SetProvider(new InMemoryProvider()); + + // Create a new client + FeatureClient client = Api.Instance.GetClient(); + + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); + + if ( v2Enabled ) + { + //Do some work + } +} +``` + +## 🌟 Features + +| Status | Features | Description | +| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Logging](#logging) | Integrate with popular logging packages. | +| βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | +| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | + +Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ + +### Providers + +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). + +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. + +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +```csharp +await Api.Instance.SetProvider(new MyProvider()); +``` + +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [named clients](#named-clients), which is covered in more detail below. + +### Targeting + +Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). + +```csharp +// set a value to the global context +EvaluationContextBuilder builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext apiCtx = builder.Build(); +Api.Instance.SetContext(apiCtx); + +// set a value to the client context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext clientCtx = builder.Build(); +var client = Api.Instance.GetClient(); +client.SetContext(clientCtx); + +// set a value to the invocation context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext reqCtx = builder.Build(); + +bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); + +``` + +### Hooks + +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/). +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. + +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. + +```csharp +// add a hook globally, to run on all evaluations +Api.Instance.AddHooks(new ExampleGlobalHook()); + +// add a hook on this client, to run on all evaluations made by this client +var client = Api.Instance.GetClient(); +client.AddHooks(new ExampleClientHook()); + +// add a hook for this evaluation only +var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); +``` + +### Logging + +The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. + +### Named clients + +Clients can be given a name. +A name is a logical identifier that can be used to associate clients with a particular provider. +If a name has no associated provider, the global provider is used. + +```csharp +// registering the default provider +await Api.Instance.SetProvider(new LocalProvider()); + +// registering a named provider +await Api.Instance.SetProvider("clientForCache", new CachedProvider()); + +// a client backed by default provider +FeatureClient clientDefault = Api.Instance.GetClient(); + +// a client backed by CachedProvider +FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); + +``` + +### Eventing + +Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, +provider readiness, or error conditions. +Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. +Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. + +Please refer to the documentation of the provider you're using to see what events are supported. + +Example usage of an Event handler: + +```csharp +public static void EventHandler(ProviderEventPayload eventDetails) +{ + Console.WriteLine(eventDetails.Type); +} +``` + +```csharp +EventHandlerDelegate callback = EventHandler; +// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event +Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +It is also possible to register an event handler for a specific client, as in the following example: + +```csharp +EventHandlerDelegate callback = EventHandler; + +var myClient = Api.Instance.GetClient("my-client"); + +var provider = new ExampleProvider(); +await Api.Instance.SetProvider(myClient.GetMetadata().Name, provider); + +myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +### Shutdown + +The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. + +```csharp +// Shut down all providers +await Api.Instance.Shutdown(); +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +```csharp +public class MyProvider : FeatureProvider +{ + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + { + // resolve a boolean flag value + } + + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + { + // resolve a double flag value + } + + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + { + // resolve a string flag value + } + + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + { + // resolve an object flag value + } +} +``` + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. + +```csharp +public class MyHook : Hook +{ + public Task Before(HookContext context, + IReadOnlyDictionary hints = null) + { + // code to run before flag evaluation + } + + public virtual Task After(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null) + { + // code to run after successful flag evaluation + } + + public virtual Task Error(HookContext context, Exception error, + IReadOnlyDictionary hints = null) + { + // code to run if there's an error during before hooks or during flag evaluation + } + + public virtual Task Finally(HookContext context, IReadOnlyDictionary hints = null) + { + // code to run after all other stages, regardless of success/failure + } +} +``` + +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! + + +## ⭐️ Support the project + +- Give this repo a ⭐️! +- Follow us on social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more information, check out our [community page](https://openfeature.dev/community/) + +## 🀝 Contributing + +Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. + +### Thanks to everyone who has already contributed + + + + + +Made with [contrib.rocks](https://contrib.rocks). + diff --git a/build/Common.prod.props b/build/Common.prod.props index 4d073ecf..f00d3aa1 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 1.3.1 + 1.4.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 3a3cd8cc..88c5fb89 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.3.1 +1.4.0 From 33db9d925478f8249e9fea7465998c8ec8da686b Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 23 Jan 2024 11:10:17 -0500 Subject: [PATCH 126/316] docs: update readme to be pure markdown (#199) Signed-off-by: Michael Beemer Co-authored-by: Austin Drenski --- README.md | 52 ++++++++++++++-------------------------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index a2db7e76..0f620a38 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,22 @@ -

- - - OpenFeature Logo - -

+ +![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) -

OpenFeature .NET SDK

+## .NET SDK - -

- - Specification - - - - - Release - - -
- - Slack - - - Codecov - - - NuGet - - - CII Best Practices - - -

+[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) +[![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) + + +[![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) +[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) -[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. @@ -100,7 +78,7 @@ public async Task Example() | βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | -Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ +_Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌_ ### Providers @@ -334,9 +312,7 @@ Interested in contributing? Great, we'd love your help! To get started, take a l ### Thanks to everyone who has already contributed - - - +[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) Made with [contrib.rocks](https://contrib.rocks). From b34fe78636dfb6b2c334a68c44ae57b72b04acbc Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 23 Jan 2024 11:32:51 -0500 Subject: [PATCH 127/316] docs: add release please tag twice The inline release please tag appears to only modify the first matching version it finds. This is an attempt to update both values. Signed-off-by: Michael Beemer --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f620a38..f6a78a51 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) -[![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) - +[![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) From aa35e253b58755b4e0b75a4b272e5a368cb5566c Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 23 Jan 2024 11:50:42 -0500 Subject: [PATCH 128/316] docs: add release please version range (#201) Signed-off-by: Michael Beemer --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6a78a51..9c96b4bf 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ ## .NET SDK -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) -[![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) + [![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) From 8dcb824e2b39e14e3b7345cd0d89f7660ca798cd Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 23 Jan 2024 11:53:56 -0500 Subject: [PATCH 129/316] docs: update release please tags Signed-off-by: Michael Beemer --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c96b4bf..fb35a637 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ ## .NET SDK -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) - [![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) + +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) + [![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) + [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) From 8b1c9d2cc26c086dcdb2434ba8646ffc07c3d063 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 23 Jan 2024 12:04:44 -0500 Subject: [PATCH 130/316] docs: fix release please tag Signed-off-by: Michael Beemer --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fb35a637..db977ca4 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ ## .NET SDK - + [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) - [![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge)](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) - +[ + ![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) From c0fbbcdb8fc71bf69a25daf6245e40533a060a74 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:10:46 -0500 Subject: [PATCH 131/316] chore(main): release 1.4.1 (#200) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 11 +++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4c313f93..4918b25e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.4.0" + ".": "1.4.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f276ce8..1a03feff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.4.1](https://github.com/open-feature/dotnet-sdk/compare/v1.4.0...v1.4.1) (2024-01-23) + + +### πŸ“š Documentation + +* add release please tag twice ([b34fe78](https://github.com/open-feature/dotnet-sdk/commit/b34fe78636dfb6b2c334a68c44ae57b72b04acbc)) +* add release please version range ([#201](https://github.com/open-feature/dotnet-sdk/issues/201)) ([aa35e25](https://github.com/open-feature/dotnet-sdk/commit/aa35e253b58755b4e0b75a4b272e5a368cb5566c)) +* fix release please tag ([8b1c9d2](https://github.com/open-feature/dotnet-sdk/commit/8b1c9d2cc26c086dcdb2434ba8646ffc07c3d063)) +* update readme to be pure markdown ([#199](https://github.com/open-feature/dotnet-sdk/issues/199)) ([33db9d9](https://github.com/open-feature/dotnet-sdk/commit/33db9d925478f8249e9fea7465998c8ec8da686b)) +* update release please tags ([8dcb824](https://github.com/open-feature/dotnet-sdk/commit/8dcb824e2b39e14e3b7345cd0d89f7660ca798cd)) + ## [1.4.0](https://github.com/open-feature/dotnet-sdk/compare/v1.3.1...v1.4.0) (2024-01-23) diff --git a/README.md b/README.md index db977ca4..a572a40f 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v1.4.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.0) + ![Release](https://img.shields.io/static/v1?label=release&message=v1.4.1&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.1) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index f00d3aa1..6735f173 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@
- 1.4.0 + 1.4.1 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 88c5fb89..347f5833 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.4.0 +1.4.1 From 45e2c862fd96092c3d20ddc5dfba46febfe802c8 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 23 Jan 2024 14:16:50 -0500 Subject: [PATCH 132/316] chore: SourceLink is built-in for .NET SDK 8.0.100+ (#198) Signed-off-by: Austin Drenski --- build/Common.prod.props | 7 ------- build/Common.props | 1 - src/Directory.Build.targets | 7 ------- 3 files changed, 15 deletions(-) diff --git a/build/Common.prod.props b/build/Common.prod.props index 6735f173..b797ea0d 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -28,11 +28,4 @@ - - true - true - true - snupkg - - diff --git a/build/Common.props b/build/Common.props index 43424f59..afd3c08a 100644 --- a/build/Common.props +++ b/build/Common.props @@ -21,7 +21,6 @@ --> [8.0.0,) [2.0,) - [1.0.0,2.0) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 44742c36..bef896bf 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,11 +1,4 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) From eba8848cb61f28b64f4a021f1534d300fcddf4eb Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 23 Jan 2024 15:09:48 -0500 Subject: [PATCH 133/316] chore: Sync release.yml with ci.yml following #173 (#195) Signed-off-by: Austin Drenski --- .github/workflows/release.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04f4aedc..01dc95a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: release-package: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - uses: google-github-actions/release-please-action@v3 @@ -34,19 +34,10 @@ jobs: if: ${{ steps.release.outputs.releases_created }} run: dotnet restore - - name: Build - if: ${{ steps.release.outputs.releases_created }} - run: | - dotnet build --configuration Release --no-restore -p:Deterministic=true - - name: Pack if: ${{ steps.release.outputs.releases_created }} - run: | - dotnet pack OpenFeature.proj --configuration Release --no-build + run: dotnet pack --no-restore - name: Publish to Nuget if: ${{ steps.release.outputs.releases_created }} - run: | - dotnet nuget push src/OpenFeature/bin/Release/OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg ` - --api-key ${{secrets.NUGET_TOKEN}} ` - --source https://api.nuget.org/v3/index.json + run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.NUGET_TOKEN }}" --source https://api.nuget.org/v3/index.json From 3722dec6cc1065b2ab9dfc7db46b4cb5f0abd910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 23 Jan 2024 21:14:38 +0000 Subject: [PATCH 134/316] build: Update OpenFeature.sln (#202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- OpenFeature.sln | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OpenFeature.sln b/OpenFeature.sln index 5ed0e809..887eb389 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -36,7 +36,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -52,10 +52,10 @@ Global {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.Build.0 = Debug|Any CPU {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.ActiveCfg = Release|Any CPU {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.Build.0 = Release|Any CPU - {90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.Build.0 = Release|Any CPU + {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -63,7 +63,7 @@ Global GlobalSection(NestedProjects) = preSolution {07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} From 0a7e98daf7d5f66f5aa8d97146e8444aa2685a33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:33:08 +1000 Subject: [PATCH 135/316] chore(deps): update actions/upload-artifact action to v4.3.0 (#203) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca5f5083..1a96f789 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.2.0 + uses: actions/upload-artifact@v4.3.0 with: name: nupkgs path: src/**/*.nupkg From f8724cd625a1f9edb33cd208aac70db3766593f1 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Wed, 24 Jan 2024 14:48:00 -0500 Subject: [PATCH 136/316] docs: update the feature table key The linter on the docs page complains that this should be a header instead of an emphasis. This PR changes the key to use a blockquote, which works better anyway. Signed-off-by: Michael Beemer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a572a40f..88c49734 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ public async Task Example() | βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | -_Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌_ +> Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ ### Providers From 249a0a8b35d0205117153e8f32948d65b7754b44 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Fri, 26 Jan 2024 15:23:52 -0500 Subject: [PATCH 137/316] chore: Enable Central Package Management (CPM) (#178) Signed-off-by: Austin Drenski --- Directory.Packages.props | 32 +++++++++++++++++++ build/Common.props | 15 ++------- build/Common.tests.props | 18 +---------- src/OpenFeature/OpenFeature.csproj | 6 ++-- .../OpenFeature.Benchmarks.csproj | 4 +-- .../OpenFeature.E2ETests.csproj | 23 +++++++------ .../OpenFeature.Tests.csproj | 16 +++++----- 7 files changed, 60 insertions(+), 54 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..e76ac475 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,32 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/Common.props b/build/Common.props index afd3c08a..b0700847 100644 --- a/build/Common.props +++ b/build/Common.props @@ -14,21 +14,12 @@ true - - - [8.0.0,) - [2.0,) - - - - + + - + diff --git a/build/Common.tests.props b/build/Common.tests.props index 060e749d..590fc99d 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -16,22 +16,6 @@ - + - - - - [4.17.0] - [0.13.1] - [3.1.2] - [6.7.0] - [2.3.3] - [17.2.0] - [5.0.0] - [2.4.3,3.0) - [2.4.1,3.0) - diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index a6a5828f..da82b999 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj index 60c2231b..81342e09 100644 --- a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj +++ b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 628b4b31..e0093787 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -7,24 +7,23 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 83689e5a..9ceac0dc 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -7,20 +7,20 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all From a509b1fb1d360ea0ac25e515ef5c7827996d4b4e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:40:54 -0500 Subject: [PATCH 138/316] chore(deps): update codecov/codecov-action action to v3.1.5 (#209) 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 5e6fb5d4..3fadffc8 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,7 +27,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v3.1.4 + - uses: codecov/codecov-action@v3.1.5 with: env_vars: OS name: Code Coverage for ${{ matrix.os }} From bac3d9483817a330044c8a13a4b3e1ffa296e009 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Fri, 26 Jan 2024 16:31:28 -0500 Subject: [PATCH 139/316] chore: More sln cleanup (#206) See: open-feature#202 Signed-off-by: Austin Drenski --- OpenFeature.sln | 70 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/OpenFeature.sln b/OpenFeature.sln index 887eb389..6f1cce8d 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -3,22 +3,61 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".", ".", "{E8916D4F-B97E-42D6-8620-ED410A106F94}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + CONTRIBUTING.md = CONTRIBUTING.md + .editorconfig = .editorconfig + .gitignore = .gitignore + .gitmodules = .gitmodules + .release-please-manifest.json = .release-please-manifest.json + CHANGELOG.md = CHANGELOG.md + CODEOWNERS = CODEOWNERS + global.json = global.json + LICENSE = LICENSE + release-please-config.json = release-please-config.json + renovate.json = renovate.json + version.txt = version.txt + EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".config", ".config", "{9392E03B-4E6B-434C-8553-B859424388B1}" ProjectSection(SolutionItems) = preProject + .config\dotnet-tools.json = .config\dotnet-tools.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C4746B8C-FE19-440B-922C-C2377F906FE8}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci.yml = .github\workflows\ci.yml .github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml - build\Common.prod.props = build\Common.prod.props - build\Common.props = build\Common.props - build\Common.tests.props = build\Common.tests.props - CONTRIBUTING.md = CONTRIBUTING.md .github\workflows\dotnet-format.yml = .github\workflows\dotnet-format.yml + .github\workflows\e2e.yml = .github\workflows\e2e.yml .github\workflows\lint-pr.yml = .github\workflows\lint-pr.yml - .github\workflows\linux-ci.yml = .github\workflows\linux-ci.yml - README.md = README.md .github\workflows\release.yml = .github\workflows\release.yml - .github\workflows\windows-ci.yml = .github\workflows\windows-ci.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{09BAB3A2-E94C-490A-861C-7D1E11BB7024}" + ProjectSection(SolutionItems) = preProject + .github\ISSUE_TEMPLATE\bug.yaml = .github\ISSUE_TEMPLATE\bug.yaml + .github\ISSUE_TEMPLATE\documentation.yaml = .github\ISSUE_TEMPLATE\documentation.yaml + .github\ISSUE_TEMPLATE\feature.yaml = .github\ISSUE_TEMPLATE\feature.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".vscode", ".vscode", "{4BB69DB3-9653-4197-9589-37FA6D658CB7}" + ProjectSection(SolutionItems) = preProject + .vscode\launch.json = .vscode\launch.json + .vscode\tasks.json = .vscode\tasks.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}" + ProjectSection(SolutionItems) = preProject + build\Common.prod.props = build\Common.prod.props + build\Common.props = build\Common.props + build\Common.tests.props = build\Common.tests.props + build\openfeature-icon.png = build\openfeature-icon.png + build\xunit.runner.json = build\xunit.runner.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C97E9975-E10A-4817-AE2C-4DD69C3C02D4}" @@ -32,6 +71,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{65FBA159-2 test\Directory.Build.props = test\Directory.Build.props EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\OpenFeature.Tests\OpenFeature.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" @@ -52,6 +93,10 @@ Global {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.Build.0 = Debug|Any CPU {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.ActiveCfg = Release|Any CPU {49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.Build.0 = Release|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.Build.0 = Release|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -63,7 +108,14 @@ Global GlobalSection(NestedProjects) = preSolution {07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} From 130654b9ae97a20c6d8964a9c0c0e0188209db55 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sat, 27 Jan 2024 15:09:03 -0500 Subject: [PATCH 140/316] chore: Sync ci.yml with contrib repo (#196) Signed-off-by: Austin Drenski --- .github/workflows/ci.yml | 54 ++++++++++++++++------------- .github/workflows/code-coverage.yml | 16 ++++++--- .github/workflows/e2e.yml | 3 ++ .github/workflows/release.yml | 3 ++ CONTRIBUTING.md | 35 +++++++++++++++++++ nuget.config | 22 ++++++++++++ 6 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 nuget.config diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a96f789..c7fc8be9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test +name: CI on: push: @@ -11,44 +11,41 @@ on: - '**.md' jobs: - unit-tests-linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 + build: + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - - - name: Run Tests - run: dotnet test test/OpenFeature.Tests/ --configuration Release --logger GitHubActions + runs-on: ${{ matrix.os }} - unit-tests-windows: - runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: recursive - name: Setup .NET SDK uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | 6.0.x 7.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore - - name: Run Tests - run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger GitHubActions + - name: Test + run: dotnet test --no-build --logger GitHubActions packaging: - needs: - - unit-tests-linux - - unit-tests-windows + needs: build permissions: contents: read @@ -57,14 +54,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive - name: Setup .NET SDK uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | 6.0.x 7.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Restore run: dotnet restore diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 3fadffc8..3a409ae8 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -12,9 +12,11 @@ on: jobs: build-test-report: - runs-on: ubuntu-latest - env: - OS: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -23,13 +25,19 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: | + 6.0.x + 7.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - uses: codecov/codecov-action@v3.1.5 with: - env_vars: OS name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true verbose: true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c0927b60..50a0b812 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -25,10 +25,13 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | 6.0.x 7.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Initialize Tests run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01dc95a8..859a9078 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,10 +25,13 @@ jobs: - name: Setup .NET SDK if: ${{ steps.release.outputs.releases_created }} uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | 6.0.x 7.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install dependencies if: ${{ steps.release.outputs.releases_created }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77eef66a..9f8cf33c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -155,3 +155,38 @@ dotnet restore dotnet build --configuration Release --output "./release" --no-restore dotnet release/OpenFeature.Benchmarks.dll ``` + +## Consuming pre-release packages + +1. Acquire a [GitHub personal access token (PAT)](https://docs.github.com/github/authenticating-to-github/creating-a-personal-access-token) scoped for `read:packages` and verify the permissions: + ```console + $ gh auth login --scopes read:packages + + ? What account do you want to log into? GitHub.com + ? What is your preferred protocol for Git operations? HTTPS + ? How would you like to authenticate GitHub CLI? Login with a web browser + + ! First copy your one-time code: ****-**** + Press Enter to open github.com in your browser... + + βœ“ Authentication complete. + - gh config set -h github.com git_protocol https + βœ“ Configured git protocol + βœ“ Logged in as ******** + ``` + + ```console + $ gh auth status + + github.com + βœ“ Logged in to github.com as ******** (~/.config/gh/hosts.yml) + βœ“ Git operations for github.com configured to use https protocol. + βœ“ Token: gho_************************************ + βœ“ Token scopes: gist, read:org, read:packages, repo, workflow + ``` +2. Run the following command to configure your local environment to consume packages from GitHub Packages: + ```console + $ dotnet nuget update source github-open-feature --username $(gh api user --jq .email) --password $(gh auth token) --store-password-in-clear-text + + Package source "github-open-feature" was successfully updated. + ``` diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..5a0edf43 --- /dev/null +++ b/nuget.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + From 7eebcdda123f9a432a8462d918b7454a26d3e389 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Mon, 29 Jan 2024 08:43:26 -0500 Subject: [PATCH 141/316] fix: Fix NU1009 reference assembly warning (#222) Signed-off-by: Austin Drenski --- Directory.Packages.props | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e76ac475..97c4baa6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,6 @@ - @@ -29,4 +28,8 @@ + + + + From 4cb3ae09375ad5f172b2e0673c9c30678939e9fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:06:43 -0500 Subject: [PATCH 142/316] chore(deps): update dependency microsoft.net.test.sdk to v17.8.0 (#216) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 97c4baa6..c0f7e1c1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + From 3be76cd562bbe942070e3c532edf40694e098440 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:10:33 -0500 Subject: [PATCH 143/316] chore(deps): update dependency nsubstitute to v5.1.0 (#217) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c0f7e1c1..d2a64b57 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From c1aece35c34e40ec911622e89882527d6815d267 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:28:30 -0500 Subject: [PATCH 144/316] chore(deps): update dependency openfeature.contrib.providers.flagd to v0.1.8 (#211) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d2a64b57..b3971d03 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + From 2c237df6e0ad278ddd8a51add202b797bf81374e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:32:51 -0500 Subject: [PATCH 145/316] chore(deps): update dependency fluentassertions to v6.12.0 (#215) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b3971d03..e4d708a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ - + From cc6c404504d9db1c234cf5642ee0c5595868774f Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Mon, 5 Feb 2024 14:45:41 -0800 Subject: [PATCH 146/316] docs: fix hook ecosystem link (#229) Signed-off-by: Michael Beemer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 88c49734..7fff5168 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); ### Hooks [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/). +Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. From 1d523cf9a457f978985fef40d37f0c7ffc2e9d3f Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Sat, 10 Feb 2024 07:22:16 +1000 Subject: [PATCH 147/316] test: Upgrade test deps (#221) ## This PR Upgrade all test deps - Autofixture 4.17 -> 4.18.1 - coverlet 3.1.2 -> 6.0.0 - FluentAssertions 6.7.0 -> 6.12.0 - MSN.Test.Sdk 17.2.0 -> 17.9.0 - NSubstitue 5 -> 5.1 - FlagD Provider 0.1.5 -> 0.1.8 - Xunit 2.4.1 -> 2.6.6 ### Related Issues Fixes #197 Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- Directory.Packages.props | 12 ++--- test/OpenFeature.Tests/OpenFeatureTests.cs | 54 +++++++++++----------- test/OpenFeature.Tests/StructureTests.cs | 4 +- test/OpenFeature.Tests/TestUtilsTest.cs | 9 ++-- test/OpenFeature.Tests/ValueTests.cs | 2 +- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e4d708a4..0714935e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,20 +12,20 @@ - + - - + + - + - - + + diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index d43bf045..02c41917 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using FluentAssertions; @@ -9,6 +10,7 @@ namespace OpenFeature.Tests { + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { [Fact] @@ -28,14 +30,14 @@ public async Task OpenFeature_Should_Initialize_Provider() var providerMockDefault = Substitute.For(); providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerMockDefault).ConfigureAwait(false); - await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerMockDefault); + await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()); var providerMockNamed = Substitute.For(); providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("the-name", providerMockNamed).ConfigureAwait(false); - await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("the-name", providerMockNamed); + await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext()); } [Fact] @@ -46,28 +48,28 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() var providerA = Substitute.For(); providerA.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); - await providerA.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerA); + await providerA.Received(1).Initialize(Api.Instance.GetContext()); var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerB).ConfigureAwait(false); - await providerB.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); - await providerA.Received(1).Shutdown().ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerB); + await providerB.Received(1).Initialize(Api.Instance.GetContext()); + await providerA.Received(1).Shutdown(); var providerC = Substitute.For(); providerC.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("named", providerC).ConfigureAwait(false); - await providerC.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerC); + await providerC.Received(1).Initialize(Api.Instance.GetContext()); var providerD = Substitute.For(); providerD.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync("named", providerD).ConfigureAwait(false); - await providerD.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false); - await providerC.Received(1).Shutdown().ConfigureAwait(false); + await Api.Instance.SetProviderAsync("named", providerD); + await providerD.Received(1).Initialize(Api.Instance.GetContext()); + await providerC.Received(1).Shutdown(); } [Fact] @@ -80,13 +82,13 @@ public async Task OpenFeature_Should_Support_Shutdown() var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); - await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false); - await Api.Instance.SetProviderAsync("named", providerB).ConfigureAwait(false); + await Api.Instance.SetProviderAsync(providerA); + await Api.Instance.SetProviderAsync("named", providerB); - await Api.Instance.Shutdown().ConfigureAwait(false); + await Api.Instance.Shutdown(); - await providerA.Received(1).Shutdown().ConfigureAwait(false); - await providerB.Received(1).Shutdown().ConfigureAwait(false); + await providerA.Received(1).Shutdown(); + await providerB.Received(1).Shutdown(); } [Fact] @@ -95,8 +97,8 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def { var openFeature = Api.Instance; - await openFeature.SetProviderAsync(new NoOpFeatureProvider()).ConfigureAwait(false); - await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(new NoOpFeatureProvider()); + await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()); var defaultClient = openFeature.GetProviderMetadata(); var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); @@ -111,7 +113,7 @@ public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() { var openFeature = Api.Instance; - await openFeature.SetProviderAsync(new TestProvider()).ConfigureAwait(false); + await openFeature.SetProviderAsync(new TestProvider()); var defaultClient = openFeature.GetProviderMetadata(); @@ -178,9 +180,9 @@ public void OpenFeature_Should_Add_Hooks() [Fact] [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] - public void OpenFeature_Should_Get_Metadata() + public async Task OpenFeature_Should_Get_Metadata() { - Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).Wait(); + await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); var openFeature = Api.Instance; var metadata = openFeature.GetProviderMetadata(); @@ -239,8 +241,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() client1.GetMetadata().Name.Should().Be("client1"); client2.GetMetadata().Name.Should().Be("client2"); - client1.GetBooleanValue("test", false).Result.Should().BeTrue(); - client2.GetBooleanValue("test", false).Result.Should().BeFalse(); + (await client1.GetBooleanValue("test", false)).Should().BeTrue(); + (await client2.GetBooleanValue("test", false)).Should().BeFalse(); } } } diff --git a/test/OpenFeature.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs index c781b83b..310303ed 100644 --- a/test/OpenFeature.Tests/StructureTests.cs +++ b/test/OpenFeature.Tests/StructureTests.cs @@ -89,7 +89,7 @@ public void Values_Should_Return_Values() var structure = Structure.Builder() .Set(KEY, VAL).Build(); - Assert.Equal(1, structure.Values.Count); + Assert.Single(structure.Values); } [Fact] @@ -100,7 +100,7 @@ public void Keys_Should_Return_Keys() var structure = Structure.Builder() .Set(KEY, VAL).Build(); - Assert.Equal(1, structure.Keys.Count); + Assert.Single(structure.Keys); Assert.Equal(0, structure.Keys.IndexOf(KEY)); } diff --git a/test/OpenFeature.Tests/TestUtilsTest.cs b/test/OpenFeature.Tests/TestUtilsTest.cs index 141194b3..1d0882b0 100644 --- a/test/OpenFeature.Tests/TestUtilsTest.cs +++ b/test/OpenFeature.Tests/TestUtilsTest.cs @@ -1,23 +1,22 @@ using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using OpenFeature.Model; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace OpenFeature.Tests { + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class TestUtilsTest { [Fact] public async void Should_Fail_If_Assertion_Fails() { - await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)).ConfigureAwait(false); + await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)); } [Fact] public async void Should_Pass_If_Assertion_Fails() { - await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))).ConfigureAwait(false); + await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))); } } } diff --git a/test/OpenFeature.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs index 4540618b..031fea9a 100644 --- a/test/OpenFeature.Tests/ValueTests.cs +++ b/test/OpenFeature.Tests/ValueTests.cs @@ -53,7 +53,7 @@ public void Int_Object_Arg_Should_Contain_Object() } catch (Exception) { - Assert.True(false, "Expected no exception."); + Assert.Fail("Expected no exception."); } } From 10820947f3d1ad0f710bccf5990b7c993956ff51 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Sat, 10 Feb 2024 14:49:12 -0500 Subject: [PATCH 148/316] feat: implement in-memory provider (#232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements in-memory provider as per spec, updates gherkin to use spec version, removes flagd deps. Signed-off-by: Todd Baert Co-authored-by: Joris Goovaerts <1333336+CommCody@users.noreply.github.com> Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/e2e.yml | 7 +- .gitmodules | 6 +- CONTRIBUTING.md | 6 - Directory.Packages.props | 1 - spec | 1 + .../{NoOpProvider.cs => Constants.cs} | 0 src/OpenFeature/Providers/Memory/Flag.cs | 78 ++++++ .../Providers/Memory/InMemoryProvider.cs | 139 ++++++++++ test-harness | 1 - .../OpenFeature.E2ETests.csproj | 1 - .../Steps/EvaluationStepDefinitions.cs | 102 +++++++- .../Providers/Memory/InMemoryProviderTests.cs | 241 ++++++++++++++++++ 12 files changed, 554 insertions(+), 29 deletions(-) create mode 160000 spec rename src/OpenFeature/Constant/{NoOpProvider.cs => Constants.cs} (100%) create mode 100644 src/OpenFeature/Providers/Memory/Flag.cs create mode 100644 src/OpenFeature/Providers/Memory/InMemoryProvider.cs delete mode 160000 test-harness create mode 100644 test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 50a0b812..4dea1592 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -13,11 +13,6 @@ on: jobs: e2e-tests: runs-on: ubuntu-latest - services: - flagd: - image: ghcr.io/open-feature/flagd-testbed:latest - ports: - - 8013:8013 steps: - uses: actions/checkout@v4 with: @@ -36,7 +31,7 @@ jobs: - name: Initialize Tests run: | git submodule update --init --recursive - cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ + cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ - name: Run Tests run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions diff --git a/.gitmodules b/.gitmodules index 61d2eb45..85115b56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "test-harness"] - path = test-harness - url = https://github.com/open-feature/test-harness.git +[submodule "spec"] + path = spec + url = https://github.com/open-feature/spec.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f8cf33c..cdac14e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,12 +67,6 @@ To be able to run the e2e tests, first we need to initialize the submodule and c git submodule update --init --recursive && cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ ``` -Afterwards, you need to start flagd locally: - -```bash -docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest -``` - Now you can run the tests using: ```bash diff --git a/Directory.Packages.props b/Directory.Packages.props index 0714935e..fe75ed4d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,6 @@ - diff --git a/spec b/spec new file mode 160000 index 00000000..b58c3b4e --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit b58c3b4ec68b0db73e6c33ed4a30e94b1ede5e85 diff --git a/src/OpenFeature/Constant/NoOpProvider.cs b/src/OpenFeature/Constant/Constants.cs similarity index 100% rename from src/OpenFeature/Constant/NoOpProvider.cs rename to src/OpenFeature/Constant/Constants.cs diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs new file mode 100644 index 00000000..99975de3 --- /dev/null +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; + +#nullable enable +namespace OpenFeature.Providers.Memory +{ + /// + /// Flag representation for the in-memory provider. + /// + public interface Flag + { + + } + + /// + /// Flag representation for the in-memory provider. + /// + public sealed class Flag : Flag + { + private Dictionary Variants; + private string DefaultVariant; + private Func? ContextEvaluator; + + /// + /// Flag representation for the in-memory provider. + /// + /// dictionary of variants and their corresponding values + /// default variant (should match 1 key in variants dictionary) + /// optional context-sensitive evaluation function + public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null) + { + this.Variants = variants; + this.DefaultVariant = defaultVariant; + this.ContextEvaluator = contextEvaluator; + } + + internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) + { + T? value = default; + if (this.ContextEvaluator == null) + { + if (this.Variants.TryGetValue(this.DefaultVariant, out value)) + { + return new ResolutionDetails( + flagKey, + value, + variant: this.DefaultVariant, + reason: Reason.Static + ); + } + else + { + throw new GeneralException($"variant {this.DefaultVariant} not found"); + } + } + else + { + var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); + if (!this.Variants.TryGetValue(variant, out value)) + { + throw new GeneralException($"variant {variant} not found"); + } + else + { + return new ResolutionDetails( + flagKey, + value, + variant: variant, + reason: Reason.TargetingMatch + ); + } + } + } + } +} diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs new file mode 100644 index 00000000..ddd1e270 --- /dev/null +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; + +#nullable enable +namespace OpenFeature.Providers.Memory +{ + /// + /// The in memory provider. + /// Useful for testing and demonstration purposes. + /// + /// In Memory Provider specification + public class InMemoryProvider : FeatureProvider + { + + private readonly Metadata _metadata = new Metadata("InMemory"); + + private Dictionary _flags; + + /// + public override Metadata GetMetadata() + { + return this._metadata; + } + + /// + /// Construct a new InMemoryProvider. + /// + /// dictionary of Flags + public InMemoryProvider(IDictionary? flags = null) + { + if (flags == null) + { + this._flags = new Dictionary(); + } + else + { + this._flags = new Dictionary(flags); // shallow copy + } + } + + /// + /// Updating provider flags configuration, replacing all flags. + /// + /// the flags to use instead of the previous flags. + public async ValueTask UpdateFlags(IDictionary? flags = null) + { + var changed = this._flags.Keys.ToList(); + if (flags == null) + { + this._flags = new Dictionary(); + } + else + { + this._flags = new Dictionary(flags); // shallow copy + } + changed.AddRange(this._flags.Keys.ToList()); + var @event = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderConfigurationChanged, + ProviderName = _metadata.Name, + FlagsChanged = changed, // emit all + Message = "flags changed", + }; + await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false); + } + + /// + public override Task> ResolveBooleanValue( + string flagKey, + bool defaultValue, + EvaluationContext? context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveStringValue( + string flagKey, + string defaultValue, + EvaluationContext? context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveIntegerValue( + string flagKey, + int defaultValue, + EvaluationContext? context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveDoubleValue( + string flagKey, + double defaultValue, + EvaluationContext? context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveStructureValue( + string flagKey, + Value defaultValue, + EvaluationContext? context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext? context) + { + if (!this._flags.TryGetValue(flagKey, out var flag)) + { + throw new FlagNotFoundException($"flag {flagKey} not found"); + } + else + { + // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. + // In a production provider, such behavior is probably not desirable; consider supporting conversion. + if (typeof(Flag).Equals(flag.GetType())) + { + return ((Flag)flag).Evaluate(flagKey, defaultValue, context); + } + else + { + throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}"); + } + } + } + } +} diff --git a/test-harness b/test-harness deleted file mode 160000 index 01c4a433..00000000 --- a/test-harness +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 01c4a433a3bcb0df6448da8c0f8030d11ce710af diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index e0093787..757c4e8f 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -16,7 +16,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive
- diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index d2cd483d..4f091ab1 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -5,8 +5,9 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using OpenFeature.Constant; -using OpenFeature.Contrib.Providers.Flagd; +using OpenFeature.Extension; using OpenFeature.Model; +using OpenFeature.Providers.Memory; using TechTalk.SpecFlow; using Xunit; @@ -41,15 +42,14 @@ public class EvaluationStepDefinitions public EvaluationStepDefinitions(ScenarioContext scenarioContext) { _scenarioContext = scenarioContext; - var flagdProvider = new FlagdProvider(); - Api.Instance.SetProviderAsync(flagdProvider).Wait(); - client = Api.Instance.GetClient(); } - [Given(@"a provider is registered with cache disabled")] - public void Givenaproviderisregisteredwithcachedisabled() + [Given(@"a provider is registered")] + public void GivenAProviderIsRegistered() { - + var memProvider = new InMemoryProvider(e2eFlagConfig); + Api.Instance.SetProviderAsync(memProvider).Wait(); + client = Api.Instance.GetClient(); } [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] @@ -247,7 +247,7 @@ public void Thenthedefaultstringvalueshouldbereturned() public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason); - Assert.Contains(errorCode, notFoundDetails.ErrorMessage); + Assert.Equal(errorCode, notFoundDetails.ErrorType.GetDescription()); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] @@ -268,8 +268,88 @@ public void Thenthedefaultintegervalueshouldbereturned() public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason); - Assert.Contains(errorCode, this.typeErrorDetails.ErrorMessage); - } - + Assert.Equal(errorCode, typeErrorDetails.ErrorType.GetDescription()); + } + + private IDictionary e2eFlagConfig = new Dictionary(){ + { + "boolean-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on" + ) + }, + { + "string-flag", new Flag( + variants: new Dictionary(){ + { "greeting", "hi" }, + { "parting", "bye" } + }, + defaultVariant: "greeting" + ) + }, + { + "integer-flag", new Flag( + variants: new Dictionary(){ + { "one", 1 }, + { "ten", 10 } + }, + defaultVariant: "ten" + ) + }, + { + "float-flag", new Flag( + variants: new Dictionary(){ + { "tenth", 0.1 }, + { "half", 0.5 } + }, + defaultVariant: "half" + ) + }, + { + "object-flag", new Flag( + variants: new Dictionary(){ + { "empty", new Value() }, + { "template", new Value(Structure.Builder() + .Set("showImages", true) + .Set("title", "Check out these pics!") + .Set("imagesPerPage", 100).Build() + ) + } + }, + defaultVariant: "template" + ) + }, + { + "context-aware", new Flag( + variants: new Dictionary(){ + { "internal", "INTERNAL" }, + { "external", "EXTERNAL" } + }, + defaultVariant: "external", + (context) => { + if (context.GetValue("fn").AsString == "SulisΕ‚aw" + && context.GetValue("ln").AsString == "ŚwiΔ™topeΕ‚k" + && context.GetValue("age").AsInteger == 29 + && context.GetValue("customer").AsBoolean == false) + { + return "internal"; + } + else return "external"; + } + ) + }, + { + "wrong-flag", new Flag( + variants: new Dictionary(){ + { "one", "uno" }, + { "two", "dos" } + }, + defaultVariant: "one" + ) + } + }; } } diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs new file mode 100644 index 00000000..3df038ab --- /dev/null +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; +using OpenFeature.Providers.Memory; +using Xunit; + +namespace OpenFeature.Tests +{ + public class InMemoryProviderTests + { + private FeatureProvider commonProvider; + + public InMemoryProviderTests() + { + var provider = new InMemoryProvider(new Dictionary(){ + { + "boolean-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on" + ) + }, + { + "string-flag", new Flag( + variants: new Dictionary(){ + { "greeting", "hi" }, + { "parting", "bye" } + }, + defaultVariant: "greeting" + ) + }, + { + "integer-flag", new Flag( + variants: new Dictionary(){ + { "one", 1 }, + { "ten", 10 } + }, + defaultVariant: "ten" + ) + }, + { + "float-flag", new Flag( + variants: new Dictionary(){ + { "tenth", 0.1 }, + { "half", 0.5 } + }, + defaultVariant: "half" + ) + }, + { + "context-aware", new Flag( + variants: new Dictionary(){ + { "internal", "INTERNAL" }, + { "external", "EXTERNAL" } + }, + defaultVariant: "external", + (context) => { + if (context.GetValue("email").AsString.Contains("@faas.com")) + { + return "internal"; + } + else return "external"; + } + ) + }, + { + "object-flag", new Flag( + variants: new Dictionary(){ + { "empty", new Value() }, + { "template", new Value(Structure.Builder() + .Set("showImages", true) + .Set("title", "Check out these pics!") + .Set("imagesPerPage", 100).Build() + ) + } + }, + defaultVariant: "template" + ) + }, + { + "invalid-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "missing" + ) + }, + { + "invalid-evaluator-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on", + (context) => { + return "missing"; + } + ) + } + }); + + this.commonProvider = provider; + } + + [Fact] + public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty).ConfigureAwait(false); + Assert.True(details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("on", details.Variant); + } + + [Fact] + public async void GetString_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal("hi", details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("greeting", details.Variant); + } + + [Fact] + public async void GetInt_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal(10, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("ten", details.Variant); + } + + [Fact] + public async void GetDouble_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal(0.5, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("half", details.Variant); + } + + [Fact] + public async void GetStruct_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal(true, details.Value.AsStructure["showImages"].AsBoolean); + Assert.Equal("Check out these pics!", details.Value.AsStructure["title"].AsString); + Assert.Equal(100, details.Value.AsStructure["imagesPerPage"].AsInteger); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("template", details.Variant); + } + + [Fact] + public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() + { + EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); + ResolutionDetails details = await this.commonProvider.ResolveStringValue("context-aware", "nope", context).ConfigureAwait(false); + Assert.Equal("INTERNAL", details.Value); + Assert.Equal(Reason.TargetingMatch, details.Reason); + Assert.Equal("internal", details.Variant); + } + + [Fact] + public async void EmptyFlags_ShouldWork() + { + var provider = new InMemoryProvider(); + await provider.UpdateFlags().ConfigureAwait(false); + Assert.Equal("InMemory", provider.GetMetadata().Name); + } + + [Fact] + public async void MissingFlag_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + } + + [Fact] + public async void MismatchedFlag_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)).ConfigureAwait(false); + } + + [Fact] + public async void MissingDefaultVariant_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + } + + [Fact] + public async void MissingEvaluatedVariant_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + } + + [Fact] + public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() + { + var provider = new InMemoryProvider(new Dictionary(){ + { + "old-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on" + ) + }}); + + ResolutionDetails details = await provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty).ConfigureAwait(false); + Assert.True(details.Value); + + // update flags + await provider.UpdateFlags(new Dictionary(){ + { + "new-flag", new Flag( + variants: new Dictionary(){ + { "greeting", "hi" }, + { "parting", "bye" } + }, + defaultVariant: "greeting" + ) + }}).ConfigureAwait(false); + + var res = await provider.GetEventChannel().Reader.ReadAsync().ConfigureAwait(false) as ProviderEventPayload; + Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res.Type); + + await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + + // new flag should be present, old gone (defaults), handler run. + ResolutionDetails detailsAfter = await provider.ResolveStringValue("new-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false); + Assert.True(details.Value); + Assert.Equal("hi", detailsAfter.Value); + } + } +} From d792b32c567b3c4ecded3fb8aab7ad9832048dcc Mon Sep 17 00:00:00 2001 From: Roelof Blom Date: Mon, 12 Feb 2024 17:46:37 +0100 Subject: [PATCH 149/316] fix: Add targeting key (#231) Fixes #230 --------- Signed-off-by: Roelof Blom Signed-off-by: Roelof Blom Signed-off-by: Austin Drenski Signed-off-by: Todd Baert Co-authored-by: Austin Drenski Co-authored-by: Todd Baert --- src/OpenFeature/Model/EvaluationContext.cs | 10 ++++- .../Model/EvaluationContextBuilder.cs | 32 +++++++++++++++- .../Steps/EvaluationStepDefinitions.cs | 13 +++---- .../OpenFeatureEvaluationContextTests.cs | 37 ++++++++++++++++++- 4 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs index e8a94bc9..6db585a1 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -16,9 +16,11 @@ public sealed class EvaluationContext /// /// Internal constructor used by the builder. /// + /// The targeting key /// The content of the context. - internal EvaluationContext(Structure content) + internal EvaluationContext(string targetingKey, Structure content) { + this.TargetingKey = targetingKey; this._structure = content; } @@ -28,6 +30,7 @@ internal EvaluationContext(Structure content) private EvaluationContext() { this._structure = Structure.Empty; + this.TargetingKey = string.Empty; } /// @@ -83,6 +86,11 @@ public IImmutableDictionary AsDictionary() /// public int Count => this._structure.Count; + /// + /// Returns the targeting key for the context. + /// + public string TargetingKey { get; } + /// /// Return an enumerator for all values /// diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs index 57afa5cf..89174cf6 100644 --- a/src/OpenFeature/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -14,11 +14,24 @@ public sealed class EvaluationContextBuilder { private readonly StructureBuilder _attributes = Structure.Builder(); + internal string TargetingKey { get; private set; } + /// /// 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.TargetingKey = targetingKey; + return this; + } + /// /// Set the key to the given . /// @@ -125,6 +138,23 @@ public EvaluationContextBuilder Set(string key, DateTime value) /// This builder public EvaluationContextBuilder Merge(EvaluationContext context) { + string newTargetingKey = ""; + + if (!string.IsNullOrWhiteSpace(TargetingKey)) + { + newTargetingKey = TargetingKey; + } + + if (!string.IsNullOrWhiteSpace(context.TargetingKey)) + { + newTargetingKey = context.TargetingKey; + } + + if (!string.IsNullOrWhiteSpace(newTargetingKey)) + { + this.TargetingKey = newTargetingKey; + } + foreach (var kvp in context) { this.Set(kvp.Key, kvp.Value); @@ -139,7 +169,7 @@ public EvaluationContextBuilder Merge(EvaluationContext context) /// An immutable public EvaluationContext Build() { - return new EvaluationContext(this._attributes.Build()); + return new EvaluationContext(this.TargetingKey, this._attributes.Build()); } } } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 4f091ab1..9aaf5fce 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -200,12 +200,11 @@ public void Giventhevariantshouldbeandthereasonshouldbe(string expectedVariant, [When(@"context contains keys ""(.*)"", ""(.*)"", ""(.*)"", ""(.*)"" with values ""(.*)"", ""(.*)"", (.*), ""(.*)""")] public void Whencontextcontainskeyswithvalues(string field1, string field2, string field3, string field4, string value1, string value2, int value3, string value4) { - var attributes = ImmutableDictionary.CreateBuilder(); - attributes.Add(field1, new Value(value1)); - attributes.Add(field2, new Value(value2)); - attributes.Add(field3, new Value(value3)); - attributes.Add(field4, new Value(bool.Parse(value4))); - this.context = new EvaluationContext(new Structure(attributes)); + this.context = new EvaluationContextBuilder() + .Set(field1, value1) + .Set(field2, value2) + .Set(field3, value3) + .Set(field4, bool.Parse(value4)).Build(); } [When(@"a flag with key ""(.*)"" is evaluated with default value ""(.*)""")] @@ -225,7 +224,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string emptyContextValue = client.GetStringValue(contextAwareFlagKey, contextAwareDefaultValue, new EvaluationContext(new Structure(ImmutableDictionary.Empty))).Result; + string emptyContextValue = client.GetStringValue(contextAwareFlagKey, contextAwareDefaultValue, new EvaluationContextBuilder().Build()).Result; Assert.Equal(expected, emptyContextValue); } diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index a9906cf4..0b8ee097 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -16,7 +16,6 @@ public void Should_Merge_Two_Contexts() .Set("key1", "value1"); var contextBuilder2 = new EvaluationContextBuilder() .Set("key2", "value2"); - var context1 = contextBuilder1.Merge(contextBuilder2.Build()).Build(); Assert.Equal(2, context1.Count); @@ -24,6 +23,35 @@ public void Should_Merge_Two_Contexts() Assert.Equal("value2", context1.GetValue("key2").AsString); } + [Fact] + public void Should_Change_TargetingKey_From_OverridingContext() + { + var contextBuilder1 = new EvaluationContextBuilder() + .Set("key1", "value1") + .SetTargetingKey("targeting_key"); + var contextBuilder2 = new EvaluationContextBuilder() + .Set("key2", "value2") + .SetTargetingKey("overriding_key"); + + var mergeContext = contextBuilder1.Merge(contextBuilder2.Build()).Build(); + + Assert.Equal("overriding_key", mergeContext.TargetingKey); + } + + [Fact] + public void Should_Retain_TargetingKey_When_OverridingContext_TargetingKey_Value_IsEmpty() + { + var contextBuilder1 = new EvaluationContextBuilder() + .Set("key1", "value1") + .SetTargetingKey("targeting_key"); + var contextBuilder2 = new EvaluationContextBuilder() + .Set("key2", "value2"); + + var mergeContext = contextBuilder1.Merge(contextBuilder2.Build()).Build(); + + Assert.Equal("targeting_key", mergeContext.TargetingKey); + } + [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.")] public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Context() @@ -51,6 +79,8 @@ public void EvaluationContext_Should_All_Types() var now = fixture.Create(); var structure = fixture.Create(); var contextBuilder = new EvaluationContextBuilder() + .SetTargetingKey("targeting_key") + .Set("targeting_key", "userId") .Set("key1", "value") .Set("key2", 1) .Set("key3", true) @@ -60,6 +90,11 @@ public void EvaluationContext_Should_All_Types() var context = contextBuilder.Build(); + context.TargetingKey.Should().Be("targeting_key"); + var targetingKeyValue = context.GetValue(context.TargetingKey); + targetingKeyValue.IsString.Should().BeTrue(); + targetingKeyValue.AsString.Should().Be("userId"); + var value1 = context.GetValue("key1"); value1.IsString.Should().BeTrue(); value1.AsString.Should().Be("value"); From fd0a54110866f3245152b28b64dedd286a752f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 12 Feb 2024 20:51:22 +0000 Subject: [PATCH 150/316] feat: Flag metadata (#223) --- src/OpenFeature/Model/BaseMetadata.cs | 76 ++++++ .../Model/FlagEvaluationDetails.cs | 11 +- src/OpenFeature/Model/FlagMetadata.cs | 28 ++ src/OpenFeature/Model/ProviderEvents.cs | 1 + src/OpenFeature/Model/ResolutionDetails.cs | 11 +- test/OpenFeature.Tests/FlagMetadataTest.cs | 246 ++++++++++++++++++ 6 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 src/OpenFeature/Model/BaseMetadata.cs create mode 100644 src/OpenFeature/Model/FlagMetadata.cs create mode 100644 test/OpenFeature.Tests/FlagMetadataTest.cs diff --git a/src/OpenFeature/Model/BaseMetadata.cs b/src/OpenFeature/Model/BaseMetadata.cs new file mode 100644 index 00000000..1e1fa211 --- /dev/null +++ b/src/OpenFeature/Model/BaseMetadata.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +#nullable enable +namespace OpenFeature.Model; + +/// +/// Represents the base class for metadata objects. +/// +public abstract class BaseMetadata +{ + private readonly ImmutableDictionary _metadata; + + internal BaseMetadata(Dictionary metadata) + { + this._metadata = metadata.ToImmutableDictionary(); + } + + /// + /// Gets the boolean value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The boolean value associated with the key, or null if the key is not found. + public virtual bool? GetBool(string key) + { + return this.GetValue(key); + } + + /// + /// Gets the integer value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The integer value associated with the key, or null if the key is not found. + public virtual int? GetInt(string key) + { + return this.GetValue(key); + } + + /// + /// Gets the double value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The double value associated with the key, or null if the key is not found. + public virtual double? GetDouble(string key) + { + return this.GetValue(key); + } + + /// + /// Gets the string value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The string value associated with the key, or null if the key is not found. + public virtual string? GetString(string key) + { + var hasValue = this._metadata.TryGetValue(key, out var value); + if (!hasValue) + { + return null; + } + + return value as string ?? null; + } + + private T? GetValue(string key) where T : struct + { + var hasValue = this._metadata.TryGetValue(key, out var value); + if (!hasValue) + { + return null; + } + + return value is T tValue ? tValue : null; + } +} diff --git a/src/OpenFeature/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs index af31ca6d..cff22a8b 100644 --- a/src/OpenFeature/Model/FlagEvaluationDetails.cs +++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs @@ -6,7 +6,7 @@ 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 { /// @@ -45,6 +45,11 @@ public sealed class FlagEvaluationDetails /// 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 FlagMetadata FlagMetadata { get; } + /// /// Initializes a new instance of the class. /// @@ -54,8 +59,9 @@ public sealed class FlagEvaluationDetails /// Reason /// Variant /// Error message + /// Flag metadata public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string reason, string variant, - string errorMessage = null) + string errorMessage = null, FlagMetadata flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; @@ -63,6 +69,7 @@ public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, strin this.Reason = reason; this.Variant = variant; this.ErrorMessage = errorMessage; + this.FlagMetadata = flagMetadata; } } } diff --git a/src/OpenFeature/Model/FlagMetadata.cs b/src/OpenFeature/Model/FlagMetadata.cs new file mode 100644 index 00000000..db666b7f --- /dev/null +++ b/src/OpenFeature/Model/FlagMetadata.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +#nullable enable +namespace OpenFeature.Model; + +/// +/// Represents the metadata associated with a feature flag. +/// +/// +public sealed class FlagMetadata : BaseMetadata +{ + /// + /// Constructor for the class. + /// + public FlagMetadata() : this([]) + { + } + + /// + /// Constructor for the class. + /// + /// The dictionary containing the metadata. + public FlagMetadata(Dictionary metadata) : base(metadata) + { + } +} diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index da68aef4..ca7c7e1a 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -36,6 +36,7 @@ public class ProviderEventPayload /// /// Metadata information for the event. /// + // TODO: This needs to be changed to a EventMetadata object public Dictionary EventMetadata { get; set; } } } diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs index 024f36de..9319096f 100644 --- a/src/OpenFeature/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -7,7 +7,7 @@ namespace OpenFeature.Model /// Describes the details of the feature flag being evaluated /// /// Flag value type - /// + /// public sealed class ResolutionDetails { /// @@ -44,6 +44,11 @@ public sealed class ResolutionDetails /// 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 FlagMetadata FlagMetadata { get; } + /// /// Initializes a new instance of the class. /// @@ -53,8 +58,9 @@ public sealed class ResolutionDetails /// Reason /// Variant /// Error message + /// Flag metadata public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string reason = null, - string variant = null, string errorMessage = null) + string variant = null, string errorMessage = null, FlagMetadata flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; @@ -62,6 +68,7 @@ public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorTyp this.Reason = reason; this.Variant = variant; this.ErrorMessage = errorMessage; + this.FlagMetadata = flagMetadata; } } } diff --git a/test/OpenFeature.Tests/FlagMetadataTest.cs b/test/OpenFeature.Tests/FlagMetadataTest.cs new file mode 100644 index 00000000..88d248de --- /dev/null +++ b/test/OpenFeature.Tests/FlagMetadataTest.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; +using Xunit; + +#nullable enable +namespace OpenFeature.Tests; + +public class FlagMetadataTest +{ + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetBool_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetBool("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetBool_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "boolKey", true + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetBool("boolKey"); + + // Assert + Assert.True(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetBool_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", "11a" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetBool("wrongKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetInt_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetInt("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetInt_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "intKey", 1 + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetInt("intKey"); + + // Assert + Assert.NotNull(result); + Assert.Equal(1, result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetInt_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", "11a" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetInt("wrongKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetDouble_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetDouble("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetDouble_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "doubleKey", 1.2 + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetDouble("doubleKey"); + + // Assert + Assert.NotNull(result); + Assert.Equal(1.2, result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetDouble_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", "11a" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetDouble("wrongKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetString_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetString("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetString_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "stringKey", "11" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetString("stringKey"); + + // Assert + Assert.NotNull(result); + Assert.Equal("11", result); + } + + [Fact] + [Specification("1.4.14", + "If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise, it MUST contain an empty record.")] + public void GetString_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", new object() + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetString("wrongKey"); + + // Assert + Assert.Null(result); + } +} From be7f2739418da7384cbc2948fc6453b3e24ae68d Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Wed, 21 Feb 2024 05:19:14 +1000 Subject: [PATCH 151/316] test: Fix up xunit warnings (#237) ## This PR Supress ConfigureAwait for unit tests as its not relevant, cleanup warnings ![image](https://github.com/open-feature/dotnet-sdk/assets/2031163/6dd21810-3938-4f81-a226-ce785951984e) Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .../Providers/Memory/InMemoryProviderTests.cs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 3df038ab..2e6cea12 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -1,15 +1,14 @@ -using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; +using System.Diagnostics.CodeAnalysis; using OpenFeature.Constant; using OpenFeature.Error; using OpenFeature.Model; using OpenFeature.Providers.Memory; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests.Providers.Memory { + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class InMemoryProviderTests { private FeatureProvider commonProvider; @@ -112,7 +111,7 @@ public InMemoryProviderTests() [Fact] public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty).ConfigureAwait(false); + ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty); Assert.True(details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("on", details.Variant); @@ -121,7 +120,7 @@ public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetString_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false); + ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty); Assert.Equal("hi", details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("greeting", details.Variant); @@ -130,7 +129,7 @@ public async void GetString_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetInt_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); + ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty); Assert.Equal(10, details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("ten", details.Variant); @@ -139,7 +138,7 @@ public async void GetInt_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetDouble_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); + ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty); Assert.Equal(0.5, details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("half", details.Variant); @@ -148,7 +147,7 @@ public async void GetDouble_ShouldEvaluateWithReasonAndVariant() [Fact] public async void GetStruct_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty).ConfigureAwait(false); + ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty); Assert.Equal(true, details.Value.AsStructure["showImages"].AsBoolean); Assert.Equal("Check out these pics!", details.Value.AsStructure["title"].AsString); Assert.Equal(100, details.Value.AsStructure["imagesPerPage"].AsInteger); @@ -160,7 +159,7 @@ public async void GetStruct_ShouldEvaluateWithReasonAndVariant() public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() { EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); - ResolutionDetails details = await this.commonProvider.ResolveStringValue("context-aware", "nope", context).ConfigureAwait(false); + ResolutionDetails details = await this.commonProvider.ResolveStringValue("context-aware", "nope", context); Assert.Equal("INTERNAL", details.Value); Assert.Equal(Reason.TargetingMatch, details.Reason); Assert.Equal("internal", details.Variant); @@ -170,32 +169,32 @@ public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( public async void EmptyFlags_ShouldWork() { var provider = new InMemoryProvider(); - await provider.UpdateFlags().ConfigureAwait(false); + await provider.UpdateFlags(); Assert.Equal("InMemory", provider.GetMetadata().Name); } [Fact] public async void MissingFlag_ShouldThrow() { - await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)); } [Fact] public async void MismatchedFlag_ShouldThrow() { - await Assert.ThrowsAsync(() => commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)).ConfigureAwait(false); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)); } [Fact] public async void MissingDefaultVariant_ShouldThrow() { - await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)); } [Fact] public async void MissingEvaluatedVariant_ShouldThrow() { - await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)); } [Fact] @@ -212,7 +211,7 @@ public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() ) }}); - ResolutionDetails details = await provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty).ConfigureAwait(false); + ResolutionDetails details = await provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty); Assert.True(details.Value); // update flags @@ -225,15 +224,15 @@ await provider.UpdateFlags(new Dictionary(){ }, defaultVariant: "greeting" ) - }}).ConfigureAwait(false); + }}); - var res = await provider.GetEventChannel().Reader.ReadAsync().ConfigureAwait(false) as ProviderEventPayload; + var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res.Type); - await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)); // new flag should be present, old gone (defaults), handler run. - ResolutionDetails detailsAfter = await provider.ResolveStringValue("new-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false); + ResolutionDetails detailsAfter = await provider.ResolveStringValue("new-flag", "nope", EvaluationContext.Empty); Assert.True(details.Value); Assert.Equal("hi", detailsAfter.Value); } From cdc1beeb00b50d47658b5fa9f053385afa227a94 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 23 Feb 2024 07:51:49 +1000 Subject: [PATCH 152/316] =?UTF-8?q?chore:=20cleanup=20unused=20usings=20?= =?UTF-8?q?=F0=9F=A7=B9=20=20(#240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR Cleanup 🧹 unused usings from the code base Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- src/OpenFeature/IFeatureClient.cs | 1 - src/OpenFeature/Model/BaseMetadata.cs | 1 - src/OpenFeature/Model/FlagMetadata.cs | 2 -- src/OpenFeature/Providers/Memory/InMemoryProvider.cs | 2 -- test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs | 4 ---- test/OpenFeature.Tests/FlagMetadataTest.cs | 1 - test/OpenFeature.Tests/TestUtils.cs | 1 - 7 files changed, 12 deletions(-) diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index 1d2e6dfb..e1550ae4 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using OpenFeature.Constant; using OpenFeature.Model; namespace OpenFeature diff --git a/src/OpenFeature/Model/BaseMetadata.cs b/src/OpenFeature/Model/BaseMetadata.cs index 1e1fa211..81f1bc50 100644 --- a/src/OpenFeature/Model/BaseMetadata.cs +++ b/src/OpenFeature/Model/BaseMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/OpenFeature/Model/FlagMetadata.cs b/src/OpenFeature/Model/FlagMetadata.cs index db666b7f..586a46bf 100644 --- a/src/OpenFeature/Model/FlagMetadata.cs +++ b/src/OpenFeature/Model/FlagMetadata.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Collections.Immutable; #nullable enable namespace OpenFeature.Model; diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index ddd1e270..e463023a 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using OpenFeature.Constant; diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 9aaf5fce..6615fb0b 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Extension; diff --git a/test/OpenFeature.Tests/FlagMetadataTest.cs b/test/OpenFeature.Tests/FlagMetadataTest.cs index 88d248de..2bb33b0d 100644 --- a/test/OpenFeature.Tests/FlagMetadataTest.cs +++ b/test/OpenFeature.Tests/FlagMetadataTest.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using OpenFeature.Model; using OpenFeature.Tests.Internal; diff --git a/test/OpenFeature.Tests/TestUtils.cs b/test/OpenFeature.Tests/TestUtils.cs index c7cfb347..15348db2 100644 --- a/test/OpenFeature.Tests/TestUtils.cs +++ b/test/OpenFeature.Tests/TestUtils.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; From cfaf1c8350a1d6754e2cfadc5daaddf2a40524e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:36:06 -0500 Subject: [PATCH 153/316] chore(deps): update actions/upload-artifact action to v4.3.1 (#233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/upload-artifact](https://togithub.com/actions/upload-artifact) | action | patch | `v4.3.0` -> `v4.3.1` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.3.1`](https://togithub.com/actions/upload-artifact/releases/tag/v4.3.1) [Compare Source](https://togithub.com/actions/upload-artifact/compare/v4.3.0...v4.3.1) - Bump [@​actions/artifacts](https://togithub.com/actions/artifacts) to latest version to include [updated GHES host check](https://togithub.com/actions/toolkit/pull/1648)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7fc8be9..591c1582 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 with: name: nupkgs path: src/**/*.nupkg From f2cb67bf40b96981f76da31242c591aeb1a2d2f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:30:48 -0500 Subject: [PATCH 154/316] chore(deps): update dependency coverlet.collector to v6.0.1 (#238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [coverlet.collector](https://togithub.com/coverlet-coverage/coverlet) | `6.0.0` -> `6.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/coverlet.collector/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/coverlet.collector/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/coverlet.collector/6.0.0/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/coverlet.collector/6.0.0/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
coverlet-coverage/coverlet (coverlet.collector) ### [`v6.0.1`](https://togithub.com/coverlet-coverage/coverlet/releases/tag/v6.0.1) ##### Fixed - Uncovered lines in .NET 8 for inheriting records [#​1555](https://togithub.com/coverlet-coverage/coverlet/issues/1555) - Fix record constructors not covered when SkipAutoProps is true [#​1561](https://togithub.com/coverlet-coverage/coverlet/issues/1561) - Fix .NET 7 Method Group branch coverage issue [#​1447](https://togithub.com/coverlet-coverage/coverlet/issues/1447) - Fix ExcludeFromCodeCoverage does not exclude method in a partial class [#​1548](https://togithub.com/coverlet-coverage/coverlet/issues/1548) - Fix ExcludeFromCodeCoverage does not exclude F# task [#​1547](https://togithub.com/coverlet-coverage/coverlet/issues/1547) - Fix issues where ExcludeFromCodeCoverage ignored [#​1431](https://togithub.com/coverlet-coverage/coverlet/issues/1431) - Fix issues with ExcludeFromCodeCoverage attribute [#​1484](https://togithub.com/coverlet-coverage/coverlet/issues/1484) - Fix broken links in documentation [#​1514](https://togithub.com/coverlet-coverage/coverlet/issues/1514) - Fix problem with coverage for .net5 WPF application [#​1221](https://togithub.com/coverlet-coverage/coverlet/issues/1221) by https://github.com/lg2de - Fix unable to instrument module for Microsoft.AspNetCore.Mvc.Razor [#​1459](https://togithub.com/coverlet-coverage/coverlet/issues/1459) by https://github.com/lg2de ##### Improvements - Extended exclude by attribute feature to work with fully qualified name [#​1589](https://togithub.com/coverlet-coverage/coverlet/issues/1589) - Use System.CommandLine instead of McMaster.Extensions.CommandLineUtils [#​1474](https://togithub.com/coverlet-coverage/coverlet/issues/1474) by https://github.com/Bertk - Fix deadlog in Coverlet.Integration.Tests.BaseTest [#​1541](https://togithub.com/coverlet-coverage/coverlet/pull/1541) by https://github.com/Bertk - Add coverlet.msbuild.tasks unit tests [#​1534](https://togithub.com/coverlet-coverage/coverlet/pull/1534) by https://github.com/Bertk [Diff between 6.0.0 and 6.0.1](https://togithub.com/coverlet-coverage/coverlet/compare/v6.0.0...v6.0.1)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Todd Baert --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fe75ed4d..56ad5c24 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + From fa25ece0444c04e2c0a12fca21064920bc09159a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:46:31 -0500 Subject: [PATCH 155/316] chore(deps): update xunit-dotnet monorepo (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [xunit](https://togithub.com/xunit/xunit) | `2.6.6` -> `2.7.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit/2.6.6/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit/2.6.6/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [xunit.runner.visualstudio](https://togithub.com/xunit/visualstudio.xunit) | `2.5.6` -> `2.5.7` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit.runner.visualstudio/2.5.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit.runner.visualstudio/2.5.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit.runner.visualstudio/2.5.6/2.5.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit.runner.visualstudio/2.5.6/2.5.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
xunit/xunit (xunit) ### [`v2.7.0`](https://togithub.com/xunit/xunit/compare/2.6.6...2.7.0) [Compare Source](https://togithub.com/xunit/xunit/compare/2.6.6...2.7.0)
xunit/visualstudio.xunit (xunit.runner.visualstudio) ### [`v2.5.7`](https://togithub.com/xunit/visualstudio.xunit/compare/2.5.6...2.5.7) [Compare Source](https://togithub.com/xunit/visualstudio.xunit/compare/2.5.6...2.5.7)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Todd Baert --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 56ad5c24..324bf73e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,8 +23,8 @@ - - + +
From 64699c8c0b5598b71fa94041797bc98d3afc8863 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Tue, 27 Feb 2024 05:54:36 +1000 Subject: [PATCH 156/316] chore: Enforce coding styles on build (#242) ## This PR A follow on from the previous PR that removed unused using, this will make sure it won't occur in the future. https://github.com/open-feature/dotnet-sdk/pull/240#issuecomment-1956892758 - Enforce code styles on build - Error on unused usings - Apply workaround for IDE0005 rule ### Related Issues Fixes https://github.com/open-feature/dotnet-sdk/pull/240#issuecomment-1956892758 Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Co-authored-by: Todd Baert --- .editorconfig | 5 ++--- build/Common.props | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2682e4d6..8a0e850a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -140,14 +140,13 @@ dotnet_diagnostic.IDE0001.severity = warning dotnet_diagnostic.IDE0002.severity = warning # IDE0005: Remove unnecessary import -# Workaround for https://github.com/dotnet/roslyn/issues/41640 -dotnet_diagnostic.IDE0005.severity = none +dotnet_diagnostic.IDE0005.severity = error # RS0041: Public members should not use oblivious types dotnet_diagnostic.RS0041.severity = suggestion # CA2007: Do not directly await a Task -dotnet_diagnostic.CA2007.severity = error +dotnet_diagnostic.CA2007.severity = errorgit [obj/**.cs] generated_code = true diff --git a/build/Common.props b/build/Common.props index b0700847..8468f7d2 100644 --- a/build/Common.props +++ b/build/Common.props @@ -3,6 +3,9 @@ latest true true + true + + EnableGenerateDocumentationFile From a577a80fc9b93fa5ddced6452da1e74f3bf9afc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:37:06 -0500 Subject: [PATCH 157/316] chore(deps): update codecov/codecov-action action to v3.1.6 (#226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [codecov/codecov-action](https://togithub.com/codecov/codecov-action) | action | patch | `v3.1.5` -> `v3.1.6` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v3.1.6`](https://togithub.com/codecov/codecov-action/releases/tag/v3.1.6) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v3.1.5...v3.1.6) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v3.1.5...v3.1.6
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](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 3a409ae8..32f57883 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -36,7 +36,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v3.1.5 + - uses: codecov/codecov-action@v3.1.6 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From 3c0075738c07e0bb2bc9875be9037f7ccbf90ac5 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Thu, 7 Mar 2024 01:01:52 +1000 Subject: [PATCH 158/316] fix: invalid editorconfig (#244) ## This PR Typo that was made under PR https://github.com/open-feature/dotnet-sdk/pull/242 Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Co-authored-by: Todd Baert --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 8a0e850a..7297b04d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -146,7 +146,7 @@ dotnet_diagnostic.IDE0005.severity = error dotnet_diagnostic.RS0041.severity = suggestion # CA2007: Do not directly await a Task -dotnet_diagnostic.CA2007.severity = errorgit +dotnet_diagnostic.CA2007.severity = error [obj/**.cs] generated_code = true From ebf55522146dad0432792bdc8cdf8772aae7d627 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 12 Mar 2024 15:33:13 -0400 Subject: [PATCH 159/316] chore: bump spec version badge (#246) Signed-off-by: Michael Beemer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fff5168..fa0d4945 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.5.2&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.5.2) +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ ![Release](https://img.shields.io/static/v1?label=release&message=v1.4.1&color=blue&style=for-the-badge) ](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.1) From b23334b9ec80342f63728d310559d33679c7e8e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:42:38 -0400 Subject: [PATCH 160/316] chore(main): release 1.5.0 (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [1.5.0](https://github.com/open-feature/dotnet-sdk/compare/v1.4.1...v1.5.0) (2024-03-12) ### πŸ› Bug Fixes * Add targeting key ([#231](https://github.com/open-feature/dotnet-sdk/issues/231)) ([d792b32](https://github.com/open-feature/dotnet-sdk/commit/d792b32c567b3c4ecded3fb8aab7ad9832048dcc)) * Fix NU1009 reference assembly warning ([#222](https://github.com/open-feature/dotnet-sdk/issues/222)) ([7eebcdd](https://github.com/open-feature/dotnet-sdk/commit/7eebcdda123f9a432a8462d918b7454a26d3e389)) * invalid editorconfig ([#244](https://github.com/open-feature/dotnet-sdk/issues/244)) ([3c00757](https://github.com/open-feature/dotnet-sdk/commit/3c0075738c07e0bb2bc9875be9037f7ccbf90ac5)) ### ✨ New Features * Flag metadata ([#223](https://github.com/open-feature/dotnet-sdk/issues/223)) ([fd0a541](https://github.com/open-feature/dotnet-sdk/commit/fd0a54110866f3245152b28b64dedd286a752f64)) * implement in-memory provider ([#232](https://github.com/open-feature/dotnet-sdk/issues/232)) ([1082094](https://github.com/open-feature/dotnet-sdk/commit/10820947f3d1ad0f710bccf5990b7c993956ff51)) ### 🧹 Chore * bump spec version badge ([#246](https://github.com/open-feature/dotnet-sdk/issues/246)) ([ebf5552](https://github.com/open-feature/dotnet-sdk/commit/ebf55522146dad0432792bdc8cdf8772aae7d627)) * cleanup unused usings 🧹 ([#240](https://github.com/open-feature/dotnet-sdk/issues/240)) ([cdc1bee](https://github.com/open-feature/dotnet-sdk/commit/cdc1beeb00b50d47658b5fa9f053385afa227a94)) * **deps:** update actions/upload-artifact action to v4.3.0 ([#203](https://github.com/open-feature/dotnet-sdk/issues/203)) ([0a7e98d](https://github.com/open-feature/dotnet-sdk/commit/0a7e98daf7d5f66f5aa8d97146e8444aa2685a33)) * **deps:** update actions/upload-artifact action to v4.3.1 ([#233](https://github.com/open-feature/dotnet-sdk/issues/233)) ([cfaf1c8](https://github.com/open-feature/dotnet-sdk/commit/cfaf1c8350a1d6754e2cfadc5daaddf2a40524e9)) * **deps:** update codecov/codecov-action action to v3.1.5 ([#209](https://github.com/open-feature/dotnet-sdk/issues/209)) ([a509b1f](https://github.com/open-feature/dotnet-sdk/commit/a509b1fb1d360ea0ac25e515ef5c7827996d4b4e)) * **deps:** update codecov/codecov-action action to v3.1.6 ([#226](https://github.com/open-feature/dotnet-sdk/issues/226)) ([a577a80](https://github.com/open-feature/dotnet-sdk/commit/a577a80fc9b93fa5ddced6452da1e74f3bf9afc7)) * **deps:** update dependency coverlet.collector to v6.0.1 ([#238](https://github.com/open-feature/dotnet-sdk/issues/238)) ([f2cb67b](https://github.com/open-feature/dotnet-sdk/commit/f2cb67bf40b96981f76da31242c591aeb1a2d2f5)) * **deps:** update dependency fluentassertions to v6.12.0 ([#215](https://github.com/open-feature/dotnet-sdk/issues/215)) ([2c237df](https://github.com/open-feature/dotnet-sdk/commit/2c237df6e0ad278ddd8a51add202b797bf81374e)) * **deps:** update dependency microsoft.net.test.sdk to v17.8.0 ([#216](https://github.com/open-feature/dotnet-sdk/issues/216)) ([4cb3ae0](https://github.com/open-feature/dotnet-sdk/commit/4cb3ae09375ad5f172b2e0673c9c30678939e9fd)) * **deps:** update dependency nsubstitute to v5.1.0 ([#217](https://github.com/open-feature/dotnet-sdk/issues/217)) ([3be76cd](https://github.com/open-feature/dotnet-sdk/commit/3be76cd562bbe942070e3c532edf40694e098440)) * **deps:** update dependency openfeature.contrib.providers.flagd to v0.1.8 ([#211](https://github.com/open-feature/dotnet-sdk/issues/211)) ([c1aece3](https://github.com/open-feature/dotnet-sdk/commit/c1aece35c34e40ec911622e89882527d6815d267)) * **deps:** update xunit-dotnet monorepo ([#236](https://github.com/open-feature/dotnet-sdk/issues/236)) ([fa25ece](https://github.com/open-feature/dotnet-sdk/commit/fa25ece0444c04e2c0a12fca21064920bc09159a)) * Enable Central Package Management (CPM) ([#178](https://github.com/open-feature/dotnet-sdk/issues/178)) ([249a0a8](https://github.com/open-feature/dotnet-sdk/commit/249a0a8b35d0205117153e8f32948d65b7754b44)) * Enforce coding styles on build ([#242](https://github.com/open-feature/dotnet-sdk/issues/242)) ([64699c8](https://github.com/open-feature/dotnet-sdk/commit/64699c8c0b5598b71fa94041797bc98d3afc8863)) * More sln cleanup ([#206](https://github.com/open-feature/dotnet-sdk/issues/206)) ([bac3d94](https://github.com/open-feature/dotnet-sdk/commit/bac3d9483817a330044c8a13a4b3e1ffa296e009)) * SourceLink is built-in for .NET SDK 8.0.100+ ([#198](https://github.com/open-feature/dotnet-sdk/issues/198)) ([45e2c86](https://github.com/open-feature/dotnet-sdk/commit/45e2c862fd96092c3d20ddc5dfba46febfe802c8)) * Sync ci.yml with contrib repo ([#196](https://github.com/open-feature/dotnet-sdk/issues/196)) ([130654b](https://github.com/open-feature/dotnet-sdk/commit/130654b9ae97a20c6d8964a9c0c0e0188209db55)) * Sync release.yml with ci.yml following [#173](https://github.com/open-feature/dotnet-sdk/issues/173) ([#195](https://github.com/open-feature/dotnet-sdk/issues/195)) ([eba8848](https://github.com/open-feature/dotnet-sdk/commit/eba8848cb61f28b64f4a021f1534d300fcddf4eb)) ### πŸ“š Documentation * fix hook ecosystem link ([#229](https://github.com/open-feature/dotnet-sdk/issues/229)) ([cc6c404](https://github.com/open-feature/dotnet-sdk/commit/cc6c404504d9db1c234cf5642ee0c5595868774f)) * update the feature table key ([f8724cd](https://github.com/open-feature/dotnet-sdk/commit/f8724cd625a1f9edb33cd208aac70db3766593f1)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4918b25e..dd8fde77 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.4.1" + ".": "1.5.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a03feff..929d2c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## [1.5.0](https://github.com/open-feature/dotnet-sdk/compare/v1.4.1...v1.5.0) (2024-03-12) + + +### πŸ› Bug Fixes + +* Add targeting key ([#231](https://github.com/open-feature/dotnet-sdk/issues/231)) ([d792b32](https://github.com/open-feature/dotnet-sdk/commit/d792b32c567b3c4ecded3fb8aab7ad9832048dcc)) +* Fix NU1009 reference assembly warning ([#222](https://github.com/open-feature/dotnet-sdk/issues/222)) ([7eebcdd](https://github.com/open-feature/dotnet-sdk/commit/7eebcdda123f9a432a8462d918b7454a26d3e389)) +* invalid editorconfig ([#244](https://github.com/open-feature/dotnet-sdk/issues/244)) ([3c00757](https://github.com/open-feature/dotnet-sdk/commit/3c0075738c07e0bb2bc9875be9037f7ccbf90ac5)) + + +### ✨ New Features + +* Flag metadata ([#223](https://github.com/open-feature/dotnet-sdk/issues/223)) ([fd0a541](https://github.com/open-feature/dotnet-sdk/commit/fd0a54110866f3245152b28b64dedd286a752f64)) +* implement in-memory provider ([#232](https://github.com/open-feature/dotnet-sdk/issues/232)) ([1082094](https://github.com/open-feature/dotnet-sdk/commit/10820947f3d1ad0f710bccf5990b7c993956ff51)) + + +### 🧹 Chore + +* bump spec version badge ([#246](https://github.com/open-feature/dotnet-sdk/issues/246)) ([ebf5552](https://github.com/open-feature/dotnet-sdk/commit/ebf55522146dad0432792bdc8cdf8772aae7d627)) +* cleanup unused usings 🧹 ([#240](https://github.com/open-feature/dotnet-sdk/issues/240)) ([cdc1bee](https://github.com/open-feature/dotnet-sdk/commit/cdc1beeb00b50d47658b5fa9f053385afa227a94)) +* **deps:** update actions/upload-artifact action to v4.3.0 ([#203](https://github.com/open-feature/dotnet-sdk/issues/203)) ([0a7e98d](https://github.com/open-feature/dotnet-sdk/commit/0a7e98daf7d5f66f5aa8d97146e8444aa2685a33)) +* **deps:** update actions/upload-artifact action to v4.3.1 ([#233](https://github.com/open-feature/dotnet-sdk/issues/233)) ([cfaf1c8](https://github.com/open-feature/dotnet-sdk/commit/cfaf1c8350a1d6754e2cfadc5daaddf2a40524e9)) +* **deps:** update codecov/codecov-action action to v3.1.5 ([#209](https://github.com/open-feature/dotnet-sdk/issues/209)) ([a509b1f](https://github.com/open-feature/dotnet-sdk/commit/a509b1fb1d360ea0ac25e515ef5c7827996d4b4e)) +* **deps:** update codecov/codecov-action action to v3.1.6 ([#226](https://github.com/open-feature/dotnet-sdk/issues/226)) ([a577a80](https://github.com/open-feature/dotnet-sdk/commit/a577a80fc9b93fa5ddced6452da1e74f3bf9afc7)) +* **deps:** update dependency coverlet.collector to v6.0.1 ([#238](https://github.com/open-feature/dotnet-sdk/issues/238)) ([f2cb67b](https://github.com/open-feature/dotnet-sdk/commit/f2cb67bf40b96981f76da31242c591aeb1a2d2f5)) +* **deps:** update dependency fluentassertions to v6.12.0 ([#215](https://github.com/open-feature/dotnet-sdk/issues/215)) ([2c237df](https://github.com/open-feature/dotnet-sdk/commit/2c237df6e0ad278ddd8a51add202b797bf81374e)) +* **deps:** update dependency microsoft.net.test.sdk to v17.8.0 ([#216](https://github.com/open-feature/dotnet-sdk/issues/216)) ([4cb3ae0](https://github.com/open-feature/dotnet-sdk/commit/4cb3ae09375ad5f172b2e0673c9c30678939e9fd)) +* **deps:** update dependency nsubstitute to v5.1.0 ([#217](https://github.com/open-feature/dotnet-sdk/issues/217)) ([3be76cd](https://github.com/open-feature/dotnet-sdk/commit/3be76cd562bbe942070e3c532edf40694e098440)) +* **deps:** update dependency openfeature.contrib.providers.flagd to v0.1.8 ([#211](https://github.com/open-feature/dotnet-sdk/issues/211)) ([c1aece3](https://github.com/open-feature/dotnet-sdk/commit/c1aece35c34e40ec911622e89882527d6815d267)) +* **deps:** update xunit-dotnet monorepo ([#236](https://github.com/open-feature/dotnet-sdk/issues/236)) ([fa25ece](https://github.com/open-feature/dotnet-sdk/commit/fa25ece0444c04e2c0a12fca21064920bc09159a)) +* Enable Central Package Management (CPM) ([#178](https://github.com/open-feature/dotnet-sdk/issues/178)) ([249a0a8](https://github.com/open-feature/dotnet-sdk/commit/249a0a8b35d0205117153e8f32948d65b7754b44)) +* Enforce coding styles on build ([#242](https://github.com/open-feature/dotnet-sdk/issues/242)) ([64699c8](https://github.com/open-feature/dotnet-sdk/commit/64699c8c0b5598b71fa94041797bc98d3afc8863)) +* More sln cleanup ([#206](https://github.com/open-feature/dotnet-sdk/issues/206)) ([bac3d94](https://github.com/open-feature/dotnet-sdk/commit/bac3d9483817a330044c8a13a4b3e1ffa296e009)) +* SourceLink is built-in for .NET SDK 8.0.100+ ([#198](https://github.com/open-feature/dotnet-sdk/issues/198)) ([45e2c86](https://github.com/open-feature/dotnet-sdk/commit/45e2c862fd96092c3d20ddc5dfba46febfe802c8)) +* Sync ci.yml with contrib repo ([#196](https://github.com/open-feature/dotnet-sdk/issues/196)) ([130654b](https://github.com/open-feature/dotnet-sdk/commit/130654b9ae97a20c6d8964a9c0c0e0188209db55)) +* Sync release.yml with ci.yml following [#173](https://github.com/open-feature/dotnet-sdk/issues/173) ([#195](https://github.com/open-feature/dotnet-sdk/issues/195)) ([eba8848](https://github.com/open-feature/dotnet-sdk/commit/eba8848cb61f28b64f4a021f1534d300fcddf4eb)) + + +### πŸ“š Documentation + +* fix hook ecosystem link ([#229](https://github.com/open-feature/dotnet-sdk/issues/229)) ([cc6c404](https://github.com/open-feature/dotnet-sdk/commit/cc6c404504d9db1c234cf5642ee0c5595868774f)) +* update the feature table key ([f8724cd](https://github.com/open-feature/dotnet-sdk/commit/f8724cd625a1f9edb33cd208aac70db3766593f1)) + ## [1.4.1](https://github.com/open-feature/dotnet-sdk/compare/v1.4.0...v1.4.1) (2024-01-23) diff --git a/README.md b/README.md index fa0d4945..6cb3c35c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v1.4.1&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.4.1) + ![Release](https://img.shields.io/static/v1?label=release&message=v1.5.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.5.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index b797ea0d..2431a810 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@
- 1.4.1 + 1.5.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 347f5833..bc80560f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.4.1 +1.5.0 From 3bdcf771852407ea461359333eac5c947171b2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Kj=C3=A6r=20Henneberg?= Date: Fri, 15 Mar 2024 04:22:24 +1300 Subject: [PATCH 161/316] ci: Generate SBOM (#245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR Generates Software Bill of Materials (SBOM) as described in #159. Once https://github.com/NuGet/Home/issues/12497 is implemented, the SBOM file(s) should be embedded in the published nuget packages. Until then, I've added the SBOM as an asset under the release. ### Known issue The SBOM file lists the dependences for all target frameworks combined. Once the above [NuGet ](https://github.com/NuGet/Home/issues/12497)issue is implemented, it should be changed, so there is one sbom created for each target framework with only the applicable references included. ### Related Issues Fixes #159 ### How to test Unfortunately, this is somewhat cumbersome to test, as the logic in question only kicks in upon a release from the main branch. I've tested it myself this way: - Create new fork of this repo - Merge this branch to main in the new repo - Create a release in the new repo Signed-off-by: Jens Henneberg Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/release.yml | 39 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 859a9078..899c3049 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - main jobs: - release-package: + release-please: runs-on: ubuntu-latest steps: @@ -16,14 +16,21 @@ jobs: command: manifest token: ${{secrets.GITHUB_TOKEN}} default-branch: main + outputs: + release_created: ${{ steps.release.outputs.release_created }} + release_tag_name: ${{ steps.release.outputs.tag_name }} + release: + runs-on: ubuntu-latest + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} + + steps: - uses: actions/checkout@v4 - if: ${{ steps.release.outputs.releases_created }} with: fetch-depth: 0 - name: Setup .NET SDK - if: ${{ steps.release.outputs.releases_created }} uses: actions/setup-dotnet@v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -34,13 +41,33 @@ jobs: source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install dependencies - if: ${{ steps.release.outputs.releases_created }} run: dotnet restore - name: Pack - if: ${{ steps.release.outputs.releases_created }} run: dotnet pack --no-restore - name: Publish to Nuget - if: ${{ steps.release.outputs.releases_created }} run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.NUGET_TOKEN }}" --source https://api.nuget.org/v3/index.json + + sbom: + runs-on: ubuntu-latest + needs: release-please + continue-on-error: true + if: ${{ needs.release-please.outputs.release_created }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install CycloneDX.NET + run: dotnet tool install CycloneDX + + - name: Generate .NET BOM + run: dotnet CycloneDX --json --exclude-dev -sv "${{ needs.release-please.outputs.release_tag_name }}" ./src/OpenFeature/OpenFeature.csproj + + - name: Attach SBOM to artifact + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + run: + gh release upload ${{ needs.release-please.outputs.release_tag_name }} bom.json From ab34c16b513ddbd0a53e925baaccd088163fbcc8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:03:23 -0400 Subject: [PATCH 162/316] chore(deps): update dependency coverlet.collector to v6.0.2 (#247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [coverlet.collector](https://togithub.com/coverlet-coverage/coverlet) | `6.0.1` -> `6.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/coverlet.collector/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/coverlet.collector/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/coverlet.collector/6.0.1/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/coverlet.collector/6.0.1/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
coverlet-coverage/coverlet (coverlet.collector) ### [`v6.0.2`](https://togithub.com/coverlet-coverage/coverlet/releases/tag/v6.0.2) ##### Fixed - Threshold-stat triggers error [#​1634](https://togithub.com/coverlet-coverage/coverlet/issues/1634) - Fixed coverlet collector 6.0.1 requires dotnet sdk 8 [#​1625](https://togithub.com/coverlet-coverage/coverlet/issues/1625) - Type initializer errors after updating from 6.0.0 to 6.0.1 [#​1629](https://togithub.com/coverlet-coverage/coverlet/issues/1629) - Exception when multiple exclude-by-attribute filters specified [#​1624](https://togithub.com/coverlet-coverage/coverlet/issues/1624) ##### Improvements - More concise options to specify multiple parameters in coverlet.console [#​1624](https://togithub.com/coverlet-coverage/coverlet/issues/1624) [Diff between 6.0.1 and 6.0.2](https://togithub.com/coverlet-coverage/coverlet/compare/v6.0.1...v6.0.2)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 324bf73e..b683b9e7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + From 79def47106b19b316b691fa195f7160ddcfb9a41 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 28 Mar 2024 08:03:21 +0800 Subject: [PATCH 163/316] chore(deps): Project file cleanup and remove unnecessary dependencies (#251) ## This PR - simplify the `InternalsVisibleTo` usage - cleanup msbuild condition - remove unnecessary dependencies --------- Signed-off-by: Weihan Li Co-authored-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- build/Common.props | 4 ++-- src/OpenFeature/OpenFeature.csproj | 15 ++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/build/Common.props b/build/Common.props index 8468f7d2..b1857e0d 100644 --- a/build/Common.props +++ b/build/Common.props @@ -18,8 +18,8 @@
- - + + diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index da82b999..9e272ba2 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -7,21 +7,14 @@ - - + - - <_Parameter1>OpenFeature.Benchmarks - - - <_Parameter1>OpenFeature.Tests - - - <_Parameter1>OpenFeature.E2ETests - + + + From 9b9c3fd09c27b191104d7ceaa726b6edd71fcd06 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 28 Mar 2024 12:02:02 -0400 Subject: [PATCH 164/316] chore: prompt 2.0 Signed-off-by: Todd Baert --- release-please-config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/release-please-config.json b/release-please-config.json index 1c1e673c..4ccbcc43 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,6 +1,7 @@ { "packages": { ".": { + "release-as": "2.0.0", "release-type": "simple", "monorepo-tags": false, "include-component-in-tag": false, From 5a5312cc082ccd880b65165135e05b4f3b035df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:32:24 +0100 Subject: [PATCH 165/316] chore!: Enable nullable reference types (#253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Enable nullable reference types - This PR enables the nullable validation and treats warnings as errors. ### Related Issues Fixes #208 ### Notes This PR turns on the nullable checks for the dotnet checks. This gives us a better and safer codebase since it checks for potential null exceptions. ### Breaking changes While this PR won't require changes to the exposed API, it might show some errors to the clients consuming it. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- build/Common.props | 2 + src/OpenFeature/Api.cs | 12 +- .../Error/FeatureProviderException.cs | 2 +- .../Error/FlagNotFoundException.cs | 2 +- src/OpenFeature/Error/GeneralException.cs | 2 +- .../Error/InvalidContextException.cs | 2 +- src/OpenFeature/Error/ParseErrorException.cs | 2 +- .../Error/ProviderNotReadyException.cs | 2 +- .../Error/TargetingKeyMissingException.cs | 2 +- .../Error/TypeMismatchException.cs | 2 +- src/OpenFeature/EventExecutor.cs | 36 ++--- src/OpenFeature/Extension/EnumExtensions.cs | 2 +- src/OpenFeature/FeatureProvider.cs | 10 +- src/OpenFeature/Hook.cs | 8 +- src/OpenFeature/IFeatureClient.cs | 20 +-- src/OpenFeature/Model/BaseMetadata.cs | 1 - src/OpenFeature/Model/ClientMetadata.cs | 4 +- src/OpenFeature/Model/EvaluationContext.cs | 6 +- .../Model/EvaluationContextBuilder.cs | 4 +- .../Model/FlagEvaluationDetails.cs | 12 +- .../Model/FlagEvaluationOptions.cs | 4 +- src/OpenFeature/Model/FlagMetadata.cs | 1 - src/OpenFeature/Model/HookContext.cs | 8 +- src/OpenFeature/Model/Metadata.cs | 4 +- src/OpenFeature/Model/ProviderEvents.cs | 10 +- src/OpenFeature/Model/ResolutionDetails.cs | 12 +- src/OpenFeature/Model/Structure.cs | 2 +- src/OpenFeature/Model/Value.cs | 10 +- src/OpenFeature/NoOpProvider.cs | 10 +- src/OpenFeature/OpenFeatureClient.cs | 56 +++---- src/OpenFeature/ProviderRepository.cs | 59 ++++--- src/OpenFeature/Providers/Memory/Flag.cs | 1 - .../Providers/Memory/InMemoryProvider.cs | 1 - .../Steps/EvaluationStepDefinitions.cs | 144 +++++++++--------- .../FeatureProviderExceptionTests.cs | 32 ++++ test/OpenFeature.Tests/FlagMetadataTest.cs | 1 - .../OpenFeatureClientTests.cs | 8 +- .../OpenFeatureEvaluationContextTests.cs | 20 ++- .../OpenFeatureEventTests.cs | 72 ++++++--- test/OpenFeature.Tests/OpenFeatureTests.cs | 14 +- .../ProviderRepositoryTests.cs | 16 +- .../Providers/Memory/InMemoryProviderTests.cs | 10 +- test/OpenFeature.Tests/StructureTests.cs | 4 +- test/OpenFeature.Tests/TestImplementations.cs | 18 +-- test/OpenFeature.Tests/ValueTests.cs | 108 ++++++++++++- 45 files changed, 486 insertions(+), 272 deletions(-) diff --git a/build/Common.props b/build/Common.props index b1857e0d..9f807b2c 100644 --- a/build/Common.props +++ b/build/Common.props @@ -6,6 +6,8 @@ true EnableGenerateDocumentationFile + enable + true diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index af302e7e..6dc0f863 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -54,7 +54,7 @@ public void SetProvider(FeatureProvider featureProvider) /// /// The provider cannot be set to null. Attempting to set the provider to null has no effect. /// Implementation of - public async Task SetProviderAsync(FeatureProvider featureProvider) + public async Task SetProviderAsync(FeatureProvider? featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); @@ -80,6 +80,10 @@ public void SetProvider(string clientName, FeatureProvider featureProvider) /// Implementation of public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider) { + if (string.IsNullOrWhiteSpace(clientName)) + { + throw new ArgumentNullException(nameof(clientName)); + } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); } @@ -138,8 +142,8 @@ public FeatureProvider GetProvider(string clientName) /// 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) => + public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null, + EvaluationContext? context = null) => new FeatureClient(name, version, logger, context); /// @@ -200,7 +204,7 @@ public void AddHooks(IEnumerable hooks) /// Sets the global /// /// The to set - public void SetContext(EvaluationContext context) + public void SetContext(EvaluationContext? context) { this._evaluationContextLock.EnterWriteLock(); try diff --git a/src/OpenFeature/Error/FeatureProviderException.cs b/src/OpenFeature/Error/FeatureProviderException.cs index df74afa4..b2c43dc7 100644 --- a/src/OpenFeature/Error/FeatureProviderException.cs +++ b/src/OpenFeature/Error/FeatureProviderException.cs @@ -20,7 +20,7 @@ public class FeatureProviderException : Exception /// Common error types /// Exception message /// Optional inner exception - public FeatureProviderException(ErrorType errorType, string message = null, Exception innerException = null) + 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 6b8fb4bb..b1a5b64a 100644 --- a/src/OpenFeature/Error/FlagNotFoundException.cs +++ b/src/OpenFeature/Error/FlagNotFoundException.cs @@ -15,7 +15,7 @@ public class FlagNotFoundException : FeatureProviderException /// /// Exception message /// Optional inner exception - public FlagNotFoundException(string message = null, Exception innerException = null) + 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 42e0fe73..4580ff31 100644 --- a/src/OpenFeature/Error/GeneralException.cs +++ b/src/OpenFeature/Error/GeneralException.cs @@ -15,7 +15,7 @@ public class GeneralException : FeatureProviderException /// /// Exception message /// Optional inner exception - public GeneralException(string message = null, Exception innerException = null) + 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 6bcc8051..ffea8ab1 100644 --- a/src/OpenFeature/Error/InvalidContextException.cs +++ b/src/OpenFeature/Error/InvalidContextException.cs @@ -15,7 +15,7 @@ public class InvalidContextException : FeatureProviderException /// /// Exception message /// Optional inner exception - public InvalidContextException(string message = null, Exception innerException = null) + 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 7b3d21e9..81ded456 100644 --- a/src/OpenFeature/Error/ParseErrorException.cs +++ b/src/OpenFeature/Error/ParseErrorException.cs @@ -15,7 +15,7 @@ public class ParseErrorException : FeatureProviderException /// /// Exception message /// Optional inner exception - public ParseErrorException(string message = null, Exception innerException = null) + public ParseErrorException(string? message = null, Exception? innerException = null) : base(ErrorType.ParseError, message, innerException) { } diff --git a/src/OpenFeature/Error/ProviderNotReadyException.cs b/src/OpenFeature/Error/ProviderNotReadyException.cs index c3c8b5d0..ca509692 100644 --- a/src/OpenFeature/Error/ProviderNotReadyException.cs +++ b/src/OpenFeature/Error/ProviderNotReadyException.cs @@ -15,7 +15,7 @@ public class ProviderNotReadyException : FeatureProviderException /// /// Exception message /// Optional inner exception - public ProviderNotReadyException(string message = null, Exception innerException = null) + 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 632cc791..71742413 100644 --- a/src/OpenFeature/Error/TargetingKeyMissingException.cs +++ b/src/OpenFeature/Error/TargetingKeyMissingException.cs @@ -15,7 +15,7 @@ public class TargetingKeyMissingException : FeatureProviderException /// /// Exception message /// Optional inner exception - public TargetingKeyMissingException(string message = null, Exception innerException = null) + 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 96c23872..83ff0cf3 100644 --- a/src/OpenFeature/Error/TypeMismatchException.cs +++ b/src/OpenFeature/Error/TypeMismatchException.cs @@ -15,7 +15,7 @@ public class TypeMismatchException : FeatureProviderException /// /// Exception message /// Optional inner exception - public TypeMismatchException(string message = null, Exception innerException = null) + 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 7bdfeb6e..a80c92d4 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -14,7 +14,7 @@ internal class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); public readonly Channel EventChannel = Channel.CreateBounded(1); - private FeatureProvider _defaultProvider; + private FeatureProvider? _defaultProvider; private readonly Dictionary _namedProviderReferences = new Dictionary(); private readonly List _activeSubscriptions = new List(); @@ -99,7 +99,7 @@ internal void RemoveClientHandler(string client, ProviderEventTypes type, EventH } } - internal void RegisterDefaultFeatureProvider(FeatureProvider provider) + internal void RegisterDefaultFeatureProvider(FeatureProvider? provider) { if (provider == null) { @@ -115,7 +115,7 @@ internal void RegisterDefaultFeatureProvider(FeatureProvider provider) } } - internal void RegisterClientFeatureProvider(string client, FeatureProvider provider) + internal void RegisterClientFeatureProvider(string client, FeatureProvider? provider) { if (provider == null) { @@ -124,7 +124,7 @@ internal void RegisterClientFeatureProvider(string client, FeatureProvider provi lock (this._lockObj) { var newProvider = provider; - FeatureProvider oldProvider = null; + FeatureProvider? oldProvider = null; if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider)) { oldProvider = foundOldProvider; @@ -136,7 +136,7 @@ internal void RegisterClientFeatureProvider(string client, FeatureProvider provi } } - 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)) @@ -174,7 +174,7 @@ private bool IsProviderActive(FeatureProvider providerRef) return this._activeSubscriptions.Contains(providerRef); } - private void EmitOnRegistration(FeatureProvider provider, ProviderEventTypes eventType, EventHandlerDelegate handler) + private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes eventType, EventHandlerDelegate handler) { if (provider == null) { @@ -202,22 +202,22 @@ private void EmitOnRegistration(FeatureProvider provider, ProviderEventTypes eve { handler.Invoke(new ProviderEventPayload { - ProviderName = provider.GetMetadata()?.Name, + ProviderName = provider.GetMetadata().Name, Type = eventType, Message = message }); } catch (Exception exc) { - this.Logger?.LogError("Error running handler: " + exc); + this.Logger.LogError(exc, "Error running handler"); } } } - private async void ProcessFeatureProviderEventsAsync(object providerRef) + private async void ProcessFeatureProviderEventsAsync(object? providerRef) { - var typedProviderRef = (FeatureProvider)providerRef; - if (typedProviderRef.GetEventChannel() is not { Reader: { } reader }) + var typedProviderRef = (FeatureProvider?)providerRef; + if (typedProviderRef?.GetEventChannel() is not { Reader: { } reader }) { return; } @@ -249,7 +249,7 @@ private async void ProcessEventAsync() case Event e: lock (this._lockObj) { - if (this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers)) + if (e.EventPayload?.Type != null && this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers)) { foreach (var eventHandler in eventHandlers) { @@ -260,11 +260,11 @@ private async void ProcessEventAsync() // look for client handlers and call invoke method there foreach (var keyAndValue in this._namedProviderReferences) { - if (keyAndValue.Value == e.Provider) + if (keyAndValue.Value == e.Provider && keyAndValue.Key != null) { if (this._clientHandlers.TryGetValue(keyAndValue.Key, out var clientRegistry)) { - if (clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) + if (e.EventPayload?.Type != null && clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) { foreach (var eventHandler in clientEventHandlers) { @@ -288,7 +288,7 @@ private async void ProcessEventAsync() // if there is an association for the client to a specific feature provider, then continue continue; } - if (keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) + if (e.EventPayload?.Type != null && keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) { foreach (var eventHandler in clientEventHandlers) { @@ -311,7 +311,7 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } catch (Exception exc) { - this.Logger?.LogError("Error running handler: " + exc); + this.Logger.LogError(exc, "Error running handler"); } } @@ -325,7 +325,7 @@ public async Task Shutdown() internal class Event { - internal FeatureProvider Provider { get; set; } - internal ProviderEventPayload EventPayload { get; set; } + 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 155bfd3e..fe10afb5 100644 --- a/src/OpenFeature/Extension/EnumExtensions.cs +++ b/src/OpenFeature/Extension/EnumExtensions.cs @@ -9,7 +9,7 @@ internal static class EnumExtensions public static string GetDescription(this Enum value) { var field = value.GetType().GetField(value.ToString()); - var attribute = field.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute; + var attribute = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute; return attribute?.Description ?? value.ToString(); } } diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 000ab0fb..bcc66558 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -45,7 +45,7 @@ public abstract class FeatureProvider /// /// public abstract Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a string feature flag @@ -55,7 +55,7 @@ public abstract Task> ResolveBooleanValue(string flagKey /// /// public abstract Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a integer feature flag @@ -65,7 +65,7 @@ public abstract Task> ResolveStringValue(string flagKe /// /// public abstract Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a double feature flag @@ -75,7 +75,7 @@ public abstract Task> ResolveIntegerValue(string flagKey, /// /// public abstract Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Resolves a structured feature flag @@ -85,7 +85,7 @@ public abstract Task> ResolveDoubleValue(string flagKe /// /// public abstract Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext context = null); + EvaluationContext? context = null); /// /// Get the status of the provider. diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs index c35c3cb4..50162729 100644 --- a/src/OpenFeature/Hook.cs +++ b/src/OpenFeature/Hook.cs @@ -29,7 +29,7 @@ public abstract class Hook /// Flag value type (bool|number|string|object) /// Modified EvaluationContext that is used for the flag evaluation public virtual Task Before(HookContext context, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.FromResult(EvaluationContext.Empty); } @@ -42,7 +42,7 @@ public virtual Task Before(HookContext context, /// Caller provided data /// Flag value type (bool|number|string|object) public virtual Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } @@ -55,7 +55,7 @@ public virtual Task After(HookContext context, FlagEvaluationDetails de /// Caller provided data /// Flag value type (bool|number|string|object) public virtual Task Error(HookContext context, Exception error, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } @@ -66,7 +66,7 @@ public virtual Task Error(HookContext context, Exception error, /// Provides context of innovation /// Caller provided data /// Flag value type (bool|number|string|object) - public virtual Task Finally(HookContext context, IReadOnlyDictionary hints = null) + public virtual Task Finally(HookContext context, IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index e1550ae4..b262f8f1 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -60,7 +60,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a boolean feature flag @@ -70,7 +70,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a string feature flag @@ -80,7 +80,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a string feature flag @@ -90,7 +90,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a integer feature flag @@ -100,7 +100,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a integer feature flag @@ -110,7 +110,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a double feature flag @@ -120,7 +120,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a double feature flag @@ -130,7 +130,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a structure object feature flag @@ -140,7 +140,7 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag value. - Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); /// /// Resolves a structure object feature flag @@ -150,6 +150,6 @@ public interface IFeatureClient : IEventBus /// Evaluation Context /// Flag Evaluation Options /// Resolved flag details - Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null); + Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); } } diff --git a/src/OpenFeature/Model/BaseMetadata.cs b/src/OpenFeature/Model/BaseMetadata.cs index 81f1bc50..876247df 100644 --- a/src/OpenFeature/Model/BaseMetadata.cs +++ b/src/OpenFeature/Model/BaseMetadata.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; -#nullable enable namespace OpenFeature.Model; /// diff --git a/src/OpenFeature/Model/ClientMetadata.cs b/src/OpenFeature/Model/ClientMetadata.cs index d006ff0d..b98e6e9d 100644 --- a/src/OpenFeature/Model/ClientMetadata.cs +++ b/src/OpenFeature/Model/ClientMetadata.cs @@ -8,14 +8,14 @@ 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) + 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 6db585a1..59b1fe20 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -18,7 +18,7 @@ public sealed class EvaluationContext /// /// The targeting key /// The content of the context. - internal EvaluationContext(string targetingKey, Structure content) + internal EvaluationContext(string? targetingKey, Structure content) { this.TargetingKey = targetingKey; this._structure = content; @@ -70,7 +70,7 @@ private EvaluationContext() /// /// Thrown when the key is /// - public bool TryGetValue(string key, out Value value) => this._structure.TryGetValue(key, out value); + public bool TryGetValue(string key, out Value? value) => this._structure.TryGetValue(key, out value); /// /// Gets all values as a Dictionary @@ -89,7 +89,7 @@ public IImmutableDictionary AsDictionary() /// /// Returns the targeting key for the context. /// - public string TargetingKey { get; } + public string? TargetingKey { get; } /// /// Return an enumerator for all values diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs index 89174cf6..1afb02fc 100644 --- a/src/OpenFeature/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -14,7 +14,7 @@ public sealed class EvaluationContextBuilder { private readonly StructureBuilder _attributes = Structure.Builder(); - internal string TargetingKey { get; private set; } + internal string? TargetingKey { get; private set; } /// /// Internal to only allow direct creation by . @@ -138,7 +138,7 @@ public EvaluationContextBuilder Set(string key, DateTime value) /// This builder public EvaluationContextBuilder Merge(EvaluationContext context) { - string newTargetingKey = ""; + string? newTargetingKey = ""; if (!string.IsNullOrWhiteSpace(TargetingKey)) { diff --git a/src/OpenFeature/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs index cff22a8b..9af2f4bf 100644 --- a/src/OpenFeature/Model/FlagEvaluationDetails.cs +++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs @@ -31,24 +31,24 @@ public sealed class FlagEvaluationDetails /// details. /// /// - public string ErrorMessage { get; } + public string? ErrorMessage { get; } /// /// Describes the reason for the outcome of the evaluation process /// - public string Reason { get; } + 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; } + 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 FlagMetadata FlagMetadata { get; } + public FlagMetadata? FlagMetadata { get; } /// /// Initializes a new instance of the class. @@ -60,8 +60,8 @@ public sealed class FlagEvaluationDetails /// Variant /// Error message /// Flag metadata - public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string reason, string variant, - string errorMessage = null, FlagMetadata flagMetadata = null) + public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string? reason, string? variant, + string? errorMessage = null, FlagMetadata? flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs index 6cf7478d..7bde600c 100644 --- a/src/OpenFeature/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs @@ -24,7 +24,7 @@ public sealed class FlagEvaluationOptions /// /// 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) + public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary? hookHints = null) { this.Hooks = hooks; this.HookHints = hookHints ?? ImmutableDictionary.Empty; @@ -35,7 +35,7 @@ public FlagEvaluationOptions(IImmutableList hooks, IImmutableDictionary /// 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) + public FlagEvaluationOptions(Hook hook, ImmutableDictionary? hookHints = null) { this.Hooks = ImmutableList.Create(hook); this.HookHints = hookHints ?? ImmutableDictionary.Empty; diff --git a/src/OpenFeature/Model/FlagMetadata.cs b/src/OpenFeature/Model/FlagMetadata.cs index 586a46bf..0fddbdd3 100644 --- a/src/OpenFeature/Model/FlagMetadata.cs +++ b/src/OpenFeature/Model/FlagMetadata.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; -#nullable enable namespace OpenFeature.Model; /// diff --git a/src/OpenFeature/Model/HookContext.cs b/src/OpenFeature/Model/HookContext.cs index 0b4a91f5..69f58bde 100644 --- a/src/OpenFeature/Model/HookContext.cs +++ b/src/OpenFeature/Model/HookContext.cs @@ -51,12 +51,12 @@ public sealed class HookContext /// Provider metadata /// Evaluation context /// When any of arguments are null - public HookContext(string flagKey, + public HookContext(string? flagKey, T defaultValue, FlagValueType flagValueType, - ClientMetadata clientMetadata, - Metadata providerMetadata, - EvaluationContext evaluationContext) + ClientMetadata? clientMetadata, + Metadata? providerMetadata, + EvaluationContext? evaluationContext) { this.FlagKey = flagKey ?? throw new ArgumentNullException(nameof(flagKey)); this.DefaultValue = defaultValue; diff --git a/src/OpenFeature/Model/Metadata.cs b/src/OpenFeature/Model/Metadata.cs index 0f8629e3..d7c972d7 100644 --- a/src/OpenFeature/Model/Metadata.cs +++ b/src/OpenFeature/Model/Metadata.cs @@ -8,13 +8,13 @@ public class Metadata /// /// Gets name of instance /// - public string Name { get; } + public string? Name { get; } /// /// Initializes a new instance of the class. /// /// Name of instance - public Metadata(string name) + public Metadata(string? name) { this.Name = name; } diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index ca7c7e1a..6feccfb0 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -6,7 +6,7 @@ namespace OpenFeature.Model /// /// The EventHandlerDelegate is an implementation of an Event Handler /// - public delegate void EventHandlerDelegate(ProviderEventPayload eventDetails); + public delegate void EventHandlerDelegate(ProviderEventPayload? eventDetails); /// /// Contains the payload of an OpenFeature Event. @@ -16,7 +16,7 @@ public class ProviderEventPayload /// /// Name of the provider. /// - public string ProviderName { get; set; } + public string? ProviderName { get; set; } /// /// Type of the event @@ -26,17 +26,17 @@ public class ProviderEventPayload /// /// A message providing more information about the event. /// - public string Message { get; set; } + public string? Message { get; set; } /// /// A List of flags that have been changed. /// - public List FlagsChanged { get; set; } + public List? FlagsChanged { get; set; } /// /// Metadata information for the event. /// // TODO: This needs to be changed to a EventMetadata object - public Dictionary EventMetadata { get; set; } + public Dictionary? EventMetadata { get; set; } } } diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs index 9319096f..5f686d47 100644 --- a/src/OpenFeature/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -29,25 +29,25 @@ public sealed class ResolutionDetails /// /// Message containing additional details about an error. /// - public string ErrorMessage { get; } + public string? ErrorMessage { get; } /// /// Describes the reason for the outcome of the evaluation process /// /// - public string Reason { get; } + 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; } + 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 FlagMetadata FlagMetadata { get; } + public FlagMetadata? FlagMetadata { get; } /// /// Initializes a new instance of the class. @@ -59,8 +59,8 @@ public sealed class ResolutionDetails /// Variant /// Error message /// Flag metadata - public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string reason = null, - string variant = null, string errorMessage = null, FlagMetadata flagMetadata = null) + public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string? reason = null, + string? variant = null, string? errorMessage = null, FlagMetadata? flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; diff --git a/src/OpenFeature/Model/Structure.cs b/src/OpenFeature/Model/Structure.cs index 8bf4b4c8..47c66923 100644 --- a/src/OpenFeature/Model/Structure.cs +++ b/src/OpenFeature/Model/Structure.cs @@ -62,7 +62,7 @@ public Structure(IDictionary attributes) /// The key of the value to be retrieved /// value to be mutated /// indicating the presence of the key. - public bool TryGetValue(string key, out Value value) => this._attributes.TryGetValue(key, out value); + public bool TryGetValue(string key, out Value? value) => this._attributes.TryGetValue(key, out value); /// /// Gets all values as a Dictionary diff --git a/src/OpenFeature/Model/Value.cs b/src/OpenFeature/Model/Value.cs index c7f60c44..5af3b8b3 100644 --- a/src/OpenFeature/Model/Value.cs +++ b/src/OpenFeature/Model/Value.cs @@ -10,7 +10,7 @@ namespace OpenFeature.Model /// public sealed class Value { - private readonly object _innerValue; + private readonly object? _innerValue; /// /// Creates a Value with the inner value set to null @@ -136,7 +136,7 @@ public Value(Object value) /// Returns the underlying inner value as an object. Returns null if the inner value is null. /// /// Value as object - public object AsObject => this._innerValue; + public object? AsObject => this._innerValue; /// /// Returns the underlying int value @@ -164,21 +164,21 @@ public Value(Object value) /// Value will be null if it isn't a string /// /// Value as string - public string AsString => this.IsString ? (string)this._innerValue : null; + public string? AsString => this.IsString ? (string?)this._innerValue : null; /// /// Returns the underlying Structure value /// Value will be null if it isn't a Structure /// /// Value as Structure - public Structure AsStructure => this.IsStructure ? (Structure)this._innerValue : null; + public Structure? AsStructure => this.IsStructure ? (Structure?)this._innerValue : null; /// /// Returns the underlying List value /// Value will be null if it isn't a List /// /// Value as List - public IImmutableList AsList => this.IsList ? (IImmutableList)this._innerValue : null; + public IImmutableList? AsList => this.IsList ? (IImmutableList?)this._innerValue : null; /// /// Returns the underlying DateTime value diff --git a/src/OpenFeature/NoOpProvider.cs b/src/OpenFeature/NoOpProvider.cs index 16ba38f4..693e504e 100644 --- a/src/OpenFeature/NoOpProvider.cs +++ b/src/OpenFeature/NoOpProvider.cs @@ -13,27 +13,27 @@ public override Metadata GetMetadata() return this._metadata; } - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext? context = null) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 56fc518f..c8513e8b 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -42,7 +42,7 @@ public sealed class FeatureClient : IFeatureClient { // Alias the provider reference so getting the method and returning the provider are // guaranteed to be the same object. - var provider = Api.Instance.GetProvider(this._metadata.Name); + var provider = Api.Instance.GetProvider(this._metadata.Name!); return (method(provider), provider); } @@ -57,7 +57,7 @@ public EvaluationContext GetContext() } /// - public void SetContext(EvaluationContext context) + public void SetContext(EvaluationContext? context) { lock (this._evaluationContextLock) { @@ -73,7 +73,7 @@ public void SetContext(EvaluationContext context) /// Logger used by client /// Context given to this client /// Throws if any of the required parameters are null - public FeatureClient(string name, string version, ILogger logger = null, EvaluationContext context = null) + public FeatureClient(string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) { this._metadata = new ClientMetadata(name, version); this._logger = logger ?? new Logger(new NullLoggerFactory()); @@ -96,13 +96,13 @@ public FeatureClient(string name, string version, ILogger logger = null, Evaluat /// public void AddHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) { - Api.Instance.AddClientHandler(this._metadata.Name, eventType, handler); + Api.Instance.AddClientHandler(this._metadata.Name!, eventType, handler); } /// public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) { - Api.Instance.RemoveClientHandler(this._metadata.Name, type, handler); + Api.Instance.RemoveClientHandler(this._metadata.Name!, type, handler); } /// @@ -136,70 +136,70 @@ public void AddHooks(IEnumerable hooks) public void ClearHooks() => this._hooks.Clear(); /// - public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetBooleanDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetBooleanDetails(string flagKey, bool defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue), FlagValueType.Boolean, flagKey, defaultValue, context, config).ConfigureAwait(false); /// - public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetStringDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetStringDetails(string flagKey, string defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue), FlagValueType.String, flagKey, defaultValue, context, config).ConfigureAwait(false); /// - public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetIntegerDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetIntegerDetails(string flagKey, int defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue), FlagValueType.Number, flagKey, defaultValue, context, config).ConfigureAwait(false); /// public async Task GetDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null, - FlagEvaluationOptions config = null) => + EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetDoubleDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetDoubleDetails(string flagKey, double defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue), FlagValueType.Number, flagKey, defaultValue, context, config).ConfigureAwait(false); /// - public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext context = null, - FlagEvaluationOptions config = null) => + public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null) => (await this.GetObjectDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; /// public async Task> GetObjectDetails(string flagKey, Value defaultValue, - EvaluationContext context = null, FlagEvaluationOptions config = null) => + EvaluationContext? context = null, FlagEvaluationOptions? config = null) => await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue), FlagValueType.Object, flagKey, defaultValue, context, config).ConfigureAwait(false); private async Task> EvaluateFlag( (Func>>, FeatureProvider) providerInfo, - FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext context = null, - FlagEvaluationOptions options = null) + FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? options = null) { var resolveValueDelegate = providerInfo.Item1; var provider = providerInfo.Item2; @@ -274,7 +274,7 @@ private async Task> EvaluateFlag( } private async Task> TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions options) + FlagEvaluationOptions? options) { var evalContextBuilder = EvaluationContext.Builder(); evalContextBuilder.Merge(context.EvaluationContext); @@ -298,7 +298,7 @@ private async Task> TriggerBeforeHooks(IReadOnlyList hoo } private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions options) + FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options) { foreach (var hook in hooks) { @@ -307,7 +307,7 @@ private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext context, Exception exception, - FlagEvaluationOptions options) + FlagEvaluationOptions? options) { foreach (var hook in hooks) { @@ -317,13 +317,13 @@ private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions options) + FlagEvaluationOptions? options) { foreach (var hook in hooks) { @@ -333,7 +333,7 @@ private async Task TriggerFinallyHooks(IReadOnlyList hooks, HookContext } catch (Exception e) { - this._logger.LogError(e, "Error while executing Finally hook {0}", hook.GetType().Name); + this._logger.LogError(e, "Error while executing Finally hook {HookName}", hook.GetType().Name); } } } diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index f95d805c..5b331d43 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -63,12 +63,12 @@ public async ValueTask DisposeAsync() /// /// called after a provider is shutdown, can be used to remove event handlers public async Task SetProvider( - FeatureProvider featureProvider, + FeatureProvider? featureProvider, EvaluationContext context, - Action afterSet = null, - Action afterInitialization = null, - Action afterError = null, - Action afterShutdown = null) + Action? afterSet = null, + Action? afterInitialization = null, + Action? afterError = null, + Action? afterShutdown = null) { // Cannot unset the feature provider. if (featureProvider == null) @@ -105,10 +105,10 @@ await InitProvider(this._defaultProvider, context, afterInitialization, afterErr } private static async Task InitProvider( - FeatureProvider newProvider, + FeatureProvider? newProvider, EvaluationContext context, - Action afterInitialization, - Action afterError) + Action? afterInitialization, + Action? afterError) { if (newProvider == null) { @@ -152,13 +152,13 @@ private static async Task InitProvider( /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - public async Task SetProvider(string clientName, - FeatureProvider featureProvider, + public async Task SetProvider(string? clientName, + FeatureProvider? featureProvider, EvaluationContext context, - Action afterSet = null, - Action afterInitialization = null, - Action afterError = null, - Action afterShutdown = null) + Action? afterSet = null, + Action? afterInitialization = null, + Action? afterError = null, + Action? afterShutdown = null) { // Cannot set a provider for a null clientName. if (clientName == null) @@ -202,16 +202,16 @@ public async Task SetProvider(string clientName, /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. /// private async Task ShutdownIfUnused( - FeatureProvider targetProvider, - Action afterShutdown, - Action afterError) + FeatureProvider? targetProvider, + Action? afterShutdown, + Action? afterError) { if (ReferenceEquals(this._defaultProvider, targetProvider)) { return; } - if (this._featureProviders.Values.Contains(targetProvider)) + if (targetProvider != null && this._featureProviders.Values.Contains(targetProvider)) { return; } @@ -228,10 +228,15 @@ private async Task ShutdownIfUnused( /// it would not be meaningful to emit an error. /// /// - private static async Task SafeShutdownProvider(FeatureProvider targetProvider, - Action afterShutdown, - Action afterError) + private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, + Action? afterShutdown, + Action? afterError) { + if (targetProvider == null) + { + return; + } + try { await targetProvider.Shutdown().ConfigureAwait(false); @@ -256,19 +261,27 @@ public FeatureProvider GetProvider() } } - public FeatureProvider GetProvider(string clientName) + public FeatureProvider GetProvider(string? clientName) { +#if NET6_0_OR_GREATER if (string.IsNullOrEmpty(clientName)) { return this.GetProvider(); } +#else + // This is a workaround for the issue in .NET Framework where string.IsNullOrEmpty is not nullable compatible. + if (clientName == null || string.IsNullOrEmpty(clientName)) + { + return this.GetProvider(); + } +#endif return this._featureProviders.TryGetValue(clientName, out var featureProvider) ? featureProvider : this.GetProvider(); } - public async Task Shutdown(Action afterError = null) + public async Task Shutdown(Action? afterError = null) { var providers = new HashSet(); this._providersLock.EnterWriteLock(); diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 99975de3..1a16bfe3 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -4,7 +4,6 @@ using OpenFeature.Error; using OpenFeature.Model; -#nullable enable namespace OpenFeature.Providers.Memory { /// diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index e463023a..766e4f3c 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -5,7 +5,6 @@ using OpenFeature.Error; using OpenFeature.Model; -#nullable enable namespace OpenFeature.Providers.Memory { /// diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 6615fb0b..b7b1f9b5 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -12,32 +12,30 @@ namespace OpenFeature.E2ETests [Binding] public class EvaluationStepDefinitions { - private readonly ScenarioContext _scenarioContext; - private static FeatureClient client; - private Task booleanFlagValue; - private Task stringFlagValue; - private Task intFlagValue; - private Task doubleFlagValue; - private Task objectFlagValue; - private Task> booleanFlagDetails; - private Task> stringFlagDetails; - private Task> intFlagDetails; - private Task> doubleFlagDetails; - private Task> objectFlagDetails; - private string contextAwareFlagKey; - private string contextAwareDefaultValue; - private string contextAwareValue; - private EvaluationContext context; - private string notFoundFlagKey; - private string notFoundDefaultValue; - private FlagEvaluationDetails notFoundDetails; - private string typeErrorFlagKey; + private static FeatureClient? client; + private Task? booleanFlagValue; + private Task? stringFlagValue; + private Task? intFlagValue; + private Task? doubleFlagValue; + private Task? objectFlagValue; + private Task>? booleanFlagDetails; + private Task>? stringFlagDetails; + private Task>? intFlagDetails; + private Task>? doubleFlagDetails; + private Task>? objectFlagDetails; + private string? contextAwareFlagKey; + private string? contextAwareDefaultValue; + private string? contextAwareValue; + private EvaluationContext? context; + private string? notFoundFlagKey; + private string? notFoundDefaultValue; + private FlagEvaluationDetails? notFoundDetails; + private string? typeErrorFlagKey; private int typeErrorDefaultValue; - private FlagEvaluationDetails typeErrorDetails; + private FlagEvaluationDetails? typeErrorDetails; public EvaluationStepDefinitions(ScenarioContext scenarioContext) { - _scenarioContext = scenarioContext; } [Given(@"a provider is registered")] @@ -45,152 +43,152 @@ public void GivenAProviderIsRegistered() { var memProvider = new InMemoryProvider(e2eFlagConfig); Api.Instance.SetProviderAsync(memProvider).Wait(); - client = Api.Instance.GetClient(); + client = Api.Instance.GetClient("TestClient", "1.0.0"); } [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagValue = client.GetBooleanValue(flagKey, defaultValue); + this.booleanFlagValue = client?.GetBooleanValue(flagKey, defaultValue); } [Then(@"the resolved boolean value should be ""(.*)""")] public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) { - Assert.Equal(expectedValue, this.booleanFlagValue.Result); + Assert.Equal(expectedValue, this.booleanFlagValue?.Result); } [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - this.stringFlagValue = client.GetStringValue(flagKey, defaultValue); + this.stringFlagValue = client?.GetStringValue(flagKey, defaultValue); } [Then(@"the resolved string value should be ""(.*)""")] public void Thentheresolvedstringvalueshouldbe(string expected) { - Assert.Equal(expected, this.stringFlagValue.Result); + Assert.Equal(expected, this.stringFlagValue?.Result); } [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) { - this.intFlagValue = client.GetIntegerValue(flagKey, defaultValue); + this.intFlagValue = client?.GetIntegerValue(flagKey, defaultValue); } [Then(@"the resolved integer value should be (.*)")] public void Thentheresolvedintegervalueshouldbe(int expected) { - Assert.Equal(expected, this.intFlagValue.Result); + Assert.Equal(expected, this.intFlagValue?.Result); } [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagValue = client.GetDoubleValue(flagKey, defaultValue); + this.doubleFlagValue = client?.GetDoubleValue(flagKey, defaultValue); } [Then(@"the resolved float value should be (.*)")] public void Thentheresolvedfloatvalueshouldbe(double expected) { - Assert.Equal(expected, this.doubleFlagValue.Result); + Assert.Equal(expected, this.doubleFlagValue?.Result); } [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) { - this.objectFlagValue = client.GetObjectValue(flagKey, new Value()); + this.objectFlagValue = client?.GetObjectValue(flagKey, new Value()); } [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) { - Value value = this.objectFlagValue.Result; - Assert.Equal(boolValue, value.AsStructure[boolField].AsBoolean); - Assert.Equal(stringValue, value.AsStructure[stringField].AsString); - Assert.Equal(numberValue, value.AsStructure[numberField].AsInteger); + Value? value = this.objectFlagValue?.Result; + Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); + Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); + Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); } [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagDetails = client.GetBooleanDetails(flagKey, defaultValue); + this.booleanFlagDetails = client?.GetBooleanDetails(flagKey, defaultValue); } [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(bool expectedValue, string expectedVariant, string expectedReason) { - var result = this.booleanFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.booleanFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) { - this.stringFlagDetails = client.GetStringDetails(flagKey, defaultValue); + this.stringFlagDetails = client?.GetStringDetails(flagKey, defaultValue); } [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(string expectedValue, string expectedVariant, string expectedReason) { - var result = this.stringFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.stringFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) { - this.intFlagDetails = client.GetIntegerDetails(flagKey, defaultValue); + this.intFlagDetails = client?.GetIntegerDetails(flagKey, defaultValue); } [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(int expectedValue, string expectedVariant, string expectedReason) { - var result = this.intFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.intFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagDetails = client.GetDoubleDetails(flagKey, defaultValue); + this.doubleFlagDetails = client?.GetDoubleDetails(flagKey, defaultValue); } [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(double expectedValue, string expectedVariant, string expectedReason) { - var result = this.doubleFlagDetails.Result; - Assert.Equal(expectedValue, result.Value); - Assert.Equal(expectedVariant, result.Variant); - Assert.Equal(expectedReason, result.Reason); + var result = this.doubleFlagDetails?.Result; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); } [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) { - this.objectFlagDetails = client.GetObjectDetails(flagKey, new Value()); + this.objectFlagDetails = client?.GetObjectDetails(flagKey, new Value()); } [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] public void Thentheresolvedobjectdetailsvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) { - Value value = this.objectFlagDetails.Result.Value; - Assert.Equal(boolValue, value.AsStructure[boolField].AsBoolean); - Assert.Equal(stringValue, value.AsStructure[stringField].AsString); - Assert.Equal(numberValue, value.AsStructure[numberField].AsInteger); + var value = this.objectFlagDetails?.Result.Value; + Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); + Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); + Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); } [Then(@"the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Giventhevariantshouldbeandthereasonshouldbe(string expectedVariant, string expectedReason) { - Assert.Equal(expectedVariant, this.objectFlagDetails.Result.Variant); - Assert.Equal(expectedReason, this.objectFlagDetails.Result.Reason); + Assert.Equal(expectedVariant, this.objectFlagDetails?.Result.Variant); + Assert.Equal(expectedReason, this.objectFlagDetails?.Result.Reason); } [When(@"context contains keys ""(.*)"", ""(.*)"", ""(.*)"", ""(.*)"" with values ""(.*)"", ""(.*)"", (.*), ""(.*)""")] @@ -208,7 +206,7 @@ public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string { contextAwareFlagKey = flagKey; contextAwareDefaultValue = defaultValue; - contextAwareValue = client.GetStringValue(flagKey, contextAwareDefaultValue, context).Result; + contextAwareValue = client?.GetStringValue(flagKey, contextAwareDefaultValue, context)?.Result; } [Then(@"the resolved string response should be ""(.*)""")] @@ -220,7 +218,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string emptyContextValue = client.GetStringValue(contextAwareFlagKey, contextAwareDefaultValue, new EvaluationContextBuilder().Build()).Result; + string? emptyContextValue = client?.GetStringValue(contextAwareFlagKey!, contextAwareDefaultValue!, new EvaluationContextBuilder().Build()).Result; Assert.Equal(expected, emptyContextValue); } @@ -229,20 +227,20 @@ public void Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultva { this.notFoundFlagKey = flagKey; this.notFoundDefaultValue = defaultValue; - this.notFoundDetails = client.GetStringDetails(this.notFoundFlagKey, this.notFoundDefaultValue).Result; + this.notFoundDetails = client?.GetStringDetails(this.notFoundFlagKey, this.notFoundDefaultValue).Result; } [Then(@"the default string value should be returned")] public void Thenthedefaultstringvalueshouldbereturned() { - Assert.Equal(this.notFoundDefaultValue, this.notFoundDetails.Value); + Assert.Equal(this.notFoundDefaultValue, this.notFoundDetails?.Value); } [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason); - Assert.Equal(errorCode, notFoundDetails.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), notFoundDetails?.Reason); + Assert.Equal(errorCode, notFoundDetails?.ErrorType.GetDescription()); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] @@ -250,20 +248,20 @@ public void Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultva { this.typeErrorFlagKey = flagKey; this.typeErrorDefaultValue = defaultValue; - this.typeErrorDetails = client.GetIntegerDetails(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; + this.typeErrorDetails = client?.GetIntegerDetails(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; } [Then(@"the default integer value should be returned")] public void Thenthedefaultintegervalueshouldbereturned() { - Assert.Equal(this.typeErrorDefaultValue, this.typeErrorDetails.Value); + Assert.Equal(this.typeErrorDefaultValue, this.typeErrorDetails?.Value); } [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason); - Assert.Equal(errorCode, typeErrorDetails.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), typeErrorDetails?.Reason); + Assert.Equal(errorCode, typeErrorDetails?.ErrorType.GetDescription()); } private IDictionary e2eFlagConfig = new Dictionary(){ diff --git a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs index 6a2f895c..fe011711 100644 --- a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs @@ -31,5 +31,37 @@ public void FeatureProviderException_Should_Allow_Custom_ErrorCode_Messages(Erro ex.Message.Should().Be(message); ex.InnerException.Should().BeOfType(); } + + private enum TestEnum + { + TestValueWithoutDescription + } + + [Fact] + public void GetDescription_WhenCalledWithEnumWithoutDescription_ReturnsEnumName() + { + // Arrange + var testEnum = TestEnum.TestValueWithoutDescription; + var expectedDescription = "TestValueWithoutDescription"; + + // Act + var actualDescription = testEnum.GetDescription(); + + // Assert + Assert.Equal(expectedDescription, actualDescription); + } + + [Fact] + public void GetDescription_WhenFieldIsNull_ReturnsEnumValueAsString() + { + // Arrange + var testEnum = (TestEnum)999;// This value should not exist in the TestEnum + + // Act + var description = testEnum.GetDescription(); + + // Assert + Assert.Equal(testEnum.ToString(), description); + } } } diff --git a/test/OpenFeature.Tests/FlagMetadataTest.cs b/test/OpenFeature.Tests/FlagMetadataTest.cs index 2bb33b0d..d716d91e 100644 --- a/test/OpenFeature.Tests/FlagMetadataTest.cs +++ b/test/OpenFeature.Tests/FlagMetadataTest.cs @@ -3,7 +3,6 @@ using OpenFeature.Tests.Internal; using Xunit; -#nullable enable namespace OpenFeature.Tests; public class FlagMetadataTest diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 86c61f83..05965c51 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -26,11 +26,12 @@ public void OpenFeatureClient_Should_Allow_Hooks() { var fixture = new Fixture(); var clientName = fixture.Create(); + var clientVersion = fixture.Create(); var hook1 = Substitute.For(); var hook2 = Substitute.For(); var hook3 = Substitute.For(); - var client = Api.Instance.GetClient(clientName); + var client = Api.Instance.GetClient(clientName, clientVersion); client.AddHooks(new[] { hook1, hook2 }); @@ -359,9 +360,12 @@ public async Task Should_Use_No_Op_When_Provider_Is_Null() [Fact] public void Should_Get_And_Set_Context() { + var fixture = new Fixture(); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); var KEY = "key"; var VAL = 1; - FeatureClient client = Api.Instance.GetClient(); + FeatureClient client = Api.Instance.GetClient(clientName, clientVersion); client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 0b8ee097..5329620f 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using AutoFixture; using FluentAssertions; using OpenFeature.Model; @@ -91,7 +92,7 @@ public void EvaluationContext_Should_All_Types() var context = contextBuilder.Build(); context.TargetingKey.Should().Be("targeting_key"); - var targetingKeyValue = context.GetValue(context.TargetingKey); + var targetingKeyValue = context.GetValue(context.TargetingKey!); targetingKeyValue.IsString.Should().BeTrue(); targetingKeyValue.AsString.Should().Be("userId"); @@ -151,5 +152,22 @@ public void Should_Be_Able_To_Get_All_Values() context.Count.Should().Be(count); } + + [Fact] + public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() + { + // Arrange + var key = "testKey"; + var expectedValue = new Value("testValue"); + var structure = new Structure(new Dictionary { { key, expectedValue } }); + var evaluationContext = new EvaluationContext("targetingKey", structure); + + // Act + var result = evaluationContext.TryGetValue(key, out var actualValue); + + // Assert + Assert.True(result); + Assert.Equal(expectedValue, actualValue); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 525241fc..599cea30 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -25,7 +25,12 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); - var eventMetadata = new Dictionary { { "foo", "bar" } }; + var eventMetadata = new Dictionary + { + { + "foo", "bar" + } + }; var myEvent = new Event { EventPayload = new ProviderEventPayload @@ -33,7 +38,10 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() Type = ProviderEventTypes.ProviderConfigurationChanged, Message = "The provider is ready", EventMetadata = eventMetadata, - FlagsChanged = new List { "flag1", "flag2" } + FlagsChanged = new List + { + "flag1", "flag2" + } } }; eventExecutor.EventChannel.Writer.TryWrite(myEvent); @@ -46,7 +54,10 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() await eventExecutor.Shutdown(); // the next event should not be propagated to the event handler - var newEventPayload = new ProviderEventPayload { Type = ProviderEventTypes.ProviderStale }; + var newEventPayload = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderStale + }; eventExecutor.EventChannel.Writer.TryWrite(newEventPayload); @@ -141,9 +152,9 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ var eventHandler = Substitute.For(); var testProvider = new TestProvider(); -#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS0618// Type or member is obsolete Api.Instance.SetProvider(testProvider); -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618// Type or member is obsolete Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -300,10 +311,12 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered() var fixture = new Fixture(); var eventHandler = Substitute.For(); - var myClient = Api.Instance.GetClient(fixture.Create()); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var myClient = Api.Instance.GetClient(clientName, clientVersion); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -326,13 +339,15 @@ public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Hand failingEventHandler.When(x => x.Invoke(Arg.Any())) .Do(x => throw new Exception()); - var myClient = Api.Instance.GetClient(fixture.Create()); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var myClient = Api.Instance.GetClient(clientName, clientVersion); myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); await Utils.AssertUntilAsync( _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) @@ -354,8 +369,8 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Pr var eventHandler = Substitute.For(); var clientEventHandler = Substitute.For(); - var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create()); - var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create()); + var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); var apiProvider = new TestProvider(fixture.Create()); var clientProvider = new TestProvider(fixture.Create()); @@ -363,7 +378,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Pr // set the default provider on API level, but not specifically to the client await Api.Instance.SetProviderAsync(apiProvider); // set the other provider specifically for the client - await Api.Instance.SetProviderAsync(myClientWithBoundProvider.GetMetadata().Name, clientProvider); + await Api.Instance.SetProviderAsync(myClientWithBoundProvider.GetMetadata().Name!, clientProvider); myClientWithNoBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); myClientWithBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, clientEventHandler); @@ -387,7 +402,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name var fixture = new Fixture(); var clientEventHandler = Substitute.For(); - var client = Api.Instance.GetClient(fixture.Create()); + var client = Api.Instance.GetClient(fixture.Create(), fixture.Create()); var defaultProvider = new TestProvider(fixture.Create()); var clientProvider = new TestProvider(fixture.Create()); @@ -401,11 +416,12 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name // verify that the client received the event from the default provider as there is no named provider registered yet await Utils.AssertUntilAsync( - _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + _ => clientEventHandler.Received(1) + .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) ); // set the other provider specifically for the client - await Api.Instance.SetProviderAsync(client.GetMetadata().Name, clientProvider); + await Api.Instance.SetProviderAsync(client.GetMetadata().Name!, clientProvider); // now, send another event for the default handler defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); @@ -417,7 +433,8 @@ await Utils.AssertUntilAsync( ); // for the default provider, the number of received events should stay unchanged await Utils.AssertUntilAsync( - _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + _ => clientEventHandler.Received(1) + .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) ); } @@ -433,10 +450,10 @@ public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_Sta var fixture = new Fixture(); var eventHandler = Substitute.For(); - var myClient = Api.Instance.GetClient(fixture.Create()); + var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); // add the event handler after the provider has already transitioned into the ready state myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -456,12 +473,12 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() var eventHandler = Substitute.For(); - var myClient = Api.Instance.GetClient(fixture.Create()); + var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, testProvider); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); // wait for the first event to be received await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); @@ -474,5 +491,20 @@ await Utils.AssertUntilAsync( _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) ); } + + [Fact] + public void RegisterClientFeatureProvider_WhenCalledWithNullProvider_DoesNotThrowException() + { + // Arrange + var eventExecutor = new EventExecutor(); + string client = "testClient"; + FeatureProvider? provider = null; + + // Act + var exception = Record.Exception(() => eventExecutor.RegisterClientFeatureProvider(client, provider)); + + // Assert + Assert.Null(exception); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 02c41917..c34a013d 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -195,7 +196,7 @@ public async Task OpenFeature_Should_Get_Metadata() [InlineData("client2", null)] [InlineData(null, null)] [Specification("1.1.6", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] - public void OpenFeature_Should_Create_Client(string name = null, string version = null) + public void OpenFeature_Should_Create_Client(string? name = null, string? version = null) { var openFeature = Api.Instance; var client = openFeature.GetClient(name, version); @@ -244,5 +245,16 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() (await client1.GetBooleanValue("test", false)).Should().BeTrue(); (await client2.GetBooleanValue("test", false)).Should().BeFalse(); } + + [Fact] + public async Task SetProviderAsync_Should_Throw_When_Null_ClientName() + { + var openFeature = Api.Instance; + + var exception = await Assert.ThrowsAsync(() => openFeature.SetProviderAsync(null!, new TestProvider())); + + exception.Should().BeOfType(); + exception.ParamName.Should().Be("clientName"); + } } } diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 62cbe9d6..0b25ebfa 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -78,14 +78,14 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provide var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; - Exception receivedError = null; + Exception? receivedError = null; await repository.SetProvider(providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; }); - Assert.Equal("BAD THINGS", receivedError.Message); + Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); } @@ -170,7 +170,7 @@ public async Task AfterError_Is_Called_For_Shutdown_That_Throws() var context = new EvaluationContextBuilder().Build(); await repository.SetProvider(provider1, context); var callCount = 0; - Exception errorThrown = null; + Exception? errorThrown = null; await repository.SetProvider(provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); @@ -178,7 +178,7 @@ await repository.SetProvider(provider2, context, afterError: (provider, ex) => callCount++; }); Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown.Message); + Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); } [Fact] @@ -244,14 +244,14 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider( var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; - Exception receivedError = null; + Exception? receivedError = null; await repository.SetProvider("the-provider", providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; }); - Assert.Equal("BAD THINGS", receivedError.Message); + Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); } @@ -337,7 +337,7 @@ public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() var context = new EvaluationContextBuilder().Build(); await repository.SetProvider("the-name", provider1, context); var callCount = 0; - Exception errorThrown = null; + Exception? errorThrown = null; await repository.SetProvider("the-name", provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); @@ -345,7 +345,7 @@ await repository.SetProvider("the-name", provider2, context, afterError: (provid callCount++; }); Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown.Message); + Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); } [Fact] diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 2e6cea12..64e1df46 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -60,7 +60,7 @@ public InMemoryProviderTests() }, defaultVariant: "external", (context) => { - if (context.GetValue("email").AsString.Contains("@faas.com")) + if (context.GetValue("email").AsString?.Contains("@faas.com") == true) { return "internal"; } @@ -148,9 +148,9 @@ public async void GetDouble_ShouldEvaluateWithReasonAndVariant() public async void GetStruct_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty); - Assert.Equal(true, details.Value.AsStructure["showImages"].AsBoolean); - Assert.Equal("Check out these pics!", details.Value.AsStructure["title"].AsString); - Assert.Equal(100, details.Value.AsStructure["imagesPerPage"].AsInteger); + Assert.Equal(true, details.Value.AsStructure?["showImages"].AsBoolean); + Assert.Equal("Check out these pics!", details.Value.AsStructure?["title"].AsString); + Assert.Equal(100, details.Value.AsStructure?["imagesPerPage"].AsInteger); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("template", details.Variant); } @@ -227,7 +227,7 @@ await provider.UpdateFlags(new Dictionary(){ }}); var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; - Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res.Type); + Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res?.Type); await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)); diff --git a/test/OpenFeature.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs index 310303ed..2dd22ae7 100644 --- a/test/OpenFeature.Tests/StructureTests.cs +++ b/test/OpenFeature.Tests/StructureTests.cs @@ -76,9 +76,9 @@ public void TryGetValue_Should_Return_Value() var structure = Structure.Builder() .Set(KEY, VAL).Build(); - Value value; + Value? value; Assert.True(structure.TryGetValue(KEY, out value)); - Assert.Equal(VAL, value.AsString); + Assert.Equal(VAL, value?.AsString); } [Fact] diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index 9683e7ef..cdb59a0c 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -11,23 +11,23 @@ public class TestHookNoOverride : Hook { } public class TestHook : Hook { - public override Task Before(HookContext context, IReadOnlyDictionary hints = null) + public override Task Before(HookContext context, IReadOnlyDictionary? hints = null) { return Task.FromResult(EvaluationContext.Empty); } public override Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) + IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } - public override Task Error(HookContext context, Exception error, IReadOnlyDictionary hints = null) + public override Task Error(HookContext context, Exception error, IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } - public override Task Finally(HookContext context, IReadOnlyDictionary hints = null) + public override Task Finally(HookContext context, IReadOnlyDictionary? hints = null) { return Task.CompletedTask; } @@ -65,31 +65,31 @@ public override Metadata GetMetadata() } public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); } public override Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } public override Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } diff --git a/test/OpenFeature.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs index 031fea9a..ec623a68 100644 --- a/test/OpenFeature.Tests/ValueTests.cs +++ b/test/OpenFeature.Tests/ValueTests.cs @@ -117,7 +117,7 @@ public void Structure_Arg_Should_Contain_Structure() Structure innerValue = Structure.Builder().Set(INNER_KEY, INNER_VALUE).Build(); Value value = new Value(innerValue); Assert.True(value.IsStructure); - Assert.Equal(INNER_VALUE, value.AsStructure.GetValue(INNER_KEY).AsString); + Assert.Equal(INNER_VALUE, value.AsStructure?.GetValue(INNER_KEY).AsString); } [Fact] @@ -127,7 +127,111 @@ public void List_Arg_Should_Contain_List() IList innerValue = new List() { new Value(ITEM_VALUE) }; Value value = new Value(innerValue); Assert.True(value.IsList); - Assert.Equal(ITEM_VALUE, value.AsList[0].AsString); + Assert.Equal(ITEM_VALUE, value.AsList?[0].AsString); + } + + [Fact] + public void Constructor_WhenCalledWithAnotherValue_CopiesInnerValue() + { + // Arrange + var originalValue = new Value("testValue"); + + // Act + var copiedValue = new Value(originalValue); + + // Assert + Assert.Equal(originalValue.AsObject, copiedValue.AsObject); + } + + [Fact] + public void AsInteger_WhenCalledWithNonIntegerInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsInteger; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsBoolean_WhenCalledWithNonBooleanInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsBoolean; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsDouble_WhenCalledWithNonDoubleInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsDouble; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsString_WhenCalledWithNonStringInnerValue_ReturnsNull() + { + // Arrange + var value = new Value(123); + + // Act + var actualValue = value.AsString; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsStructure_WhenCalledWithNonStructureInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsStructure; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsList_WhenCalledWithNonListInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsList; + + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsDateTime_WhenCalledWithNonDateTimeInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsDateTime; + + // Assert + Assert.Null(actualValue); } } } From e6542222827cc25cd5a1acc5af47ce55149c0623 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:23:25 +0100 Subject: [PATCH 166/316] chore(deps): update dependency coverlet.msbuild to v6.0.2 (#239) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b683b9e7..b57473e4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + From 949d53cada68bee8e80d113357fa6df8d425d3c1 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Tue, 9 Apr 2024 14:28:11 +0200 Subject: [PATCH 167/316] fix: Add missing error message when an error occurred (#256) ## This PR - Add missing error message when an error occurred ### Related Issues Fixes #255 --------- Signed-off-by: Thomas Poignant --- src/OpenFeature/OpenFeatureClient.cs | 2 +- test/OpenFeature.Tests/OpenFeatureClientTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index c8513e8b..77cd8754 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -262,7 +262,7 @@ private async Task> EvaluateFlag( { this._logger.LogError(ex, "Error while evaluating flag {FlagKey}", flagKey); var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General; - evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty); + evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty, ex.Message); await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); } finally diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 05965c51..9509eb8a 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -178,6 +178,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); + evaluationDetails.ErrorMessage.Should().Be(new InvalidCastException().Message); _ = mockedFeatureProvider.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); From d70e27f387d12cf17ef7313ab1aaba0c6c8b7452 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 9 Apr 2024 17:27:10 -0400 Subject: [PATCH 168/316] chore: Use logging source generators (#189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates `OpenFeature` to use the latest MELA source generators to produce compile-time logging delegates. The most obvious critique of this PR is likely to be that, much like #188, this PR changes the dependency graph by bumping our MELA dependency from `[2.0.0,) => [8.0.0,)`. To that^ critique, I continue to contend that depending on an ancient version of MELA is unlikely to aid many real-world consumers of this SDK, since new versions of MELA provide robust TFM coverage, as well as my personal disbelief that anyone looking to consume this library in 2024 is deploying an app that won't already have transitively referenced MELA `8.0.0` somewhere in its package graph. _(If you are a user or potential user of this SDK and have a real-world use case of a legacy app that __does not and cannot__ reference MELA `>= 8.0.0`, please ping back here! Would love to hear from you and adjust my disbelief accordingly!)_ _(Note: if this PR lands before #188, then I'll update #188 to remove its added dependency on `Microsoft.Bcl.AsyncInterfaces`, since it flows transitively from MELA `8.0.0`.)_ Upon request, I am happy to provide a soapbox diatribe on why I think we should care about source generators, hook perf, and incidental logging allocations, especially as an SDK that wants to be trusted and baked into all kinds of consumer apps, but eliding that for now in favor of some docs refs: - https://learn.microsoft.com/dotnet/core/extensions/high-performance-logging - https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator Signed-off-by: Austin Drenski Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 3 +- build/Common.props | 1 + src/OpenFeature/Api.cs | 2 +- src/OpenFeature/EventExecutor.cs | 15 ++++++---- src/OpenFeature/OpenFeatureClient.cs | 29 ++++++++++++++----- .../OpenFeatureClientTests.cs | 8 +---- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b57473e4..9b67b6aa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,9 +6,10 @@ - + + diff --git a/build/Common.props b/build/Common.props index 9f807b2c..5cc12515 100644 --- a/build/Common.props +++ b/build/Common.props @@ -22,6 +22,7 @@ + diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 6dc0f863..fbafa695 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -280,7 +280,7 @@ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) /// The logger to be used public void SetLogger(ILogger logger) { - this._eventExecutor.Logger = logger; + this._eventExecutor.SetLogger(logger); } internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler) diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index a80c92d4..816bf13e 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -10,7 +10,7 @@ namespace OpenFeature { - internal class EventExecutor : IAsyncDisposable + internal sealed partial class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); public readonly Channel EventChannel = Channel.CreateBounded(1); @@ -21,17 +21,19 @@ internal class EventExecutor : IAsyncDisposable private readonly Dictionary> _apiHandlers = new Dictionary>(); private readonly Dictionary>> _clientHandlers = new Dictionary>>(); - internal ILogger Logger { get; set; } + private ILogger _logger; public EventExecutor() { - this.Logger = new Logger(new NullLoggerFactory()); + this._logger = NullLogger.Instance; var eventProcessing = new Thread(this.ProcessEventAsync); eventProcessing.Start(); } public ValueTask DisposeAsync() => new(this.Shutdown()); + internal void SetLogger(ILogger logger) => this._logger = logger; + internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) { lock (this._lockObj) @@ -209,7 +211,7 @@ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes ev } catch (Exception exc) { - this.Logger.LogError(exc, "Error running handler"); + this.ErrorRunningHandler(exc); } } } @@ -311,7 +313,7 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } catch (Exception exc) { - this.Logger.LogError(exc, "Error running handler"); + this.ErrorRunningHandler(exc); } } @@ -321,6 +323,9 @@ public async Task Shutdown() await this.EventChannel.Reader.Completion.ConfigureAwait(false); } + + [LoggerMessage(100, LogLevel.Error, "Error running handler")] + partial void ErrorRunningHandler(Exception exception); } internal class Event diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 77cd8754..6145094e 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -15,7 +15,7 @@ namespace OpenFeature /// /// /// - public sealed class FeatureClient : IFeatureClient + public sealed partial class FeatureClient : IFeatureClient { private readonly ClientMetadata _metadata; private readonly ConcurrentStack _hooks = new ConcurrentStack(); @@ -76,7 +76,7 @@ public void SetContext(EvaluationContext? context) public FeatureClient(string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) { this._metadata = new ClientMetadata(name, version); - this._logger = logger ?? new Logger(new NullLoggerFactory()); + this._logger = logger ?? NullLogger.Instance; this._evaluationContext = context ?? EvaluationContext.Empty; } @@ -252,15 +252,14 @@ private async Task> EvaluateFlag( } catch (FeatureProviderException ex) { - this._logger.LogError(ex, "Error while evaluating flag {FlagKey}. Error {ErrorType}", flagKey, - ex.ErrorType.GetDescription()); + this.FlagEvaluationErrorWithDescription(flagKey, ex.ErrorType.GetDescription(), ex); evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error, string.Empty, ex.Message); await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); } catch (Exception ex) { - this._logger.LogError(ex, "Error while evaluating flag {FlagKey}", flagKey); + this.FlagEvaluationError(flagKey, ex); var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General; evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty, ex.Message); await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); @@ -289,8 +288,7 @@ private async Task> TriggerBeforeHooks(IReadOnlyList hoo } else { - this._logger.LogDebug("Hook {HookName} returned null, nothing to merge back into context", - hook.GetType().Name); + this.HookReturnedNull(hook.GetType().Name); } } @@ -317,7 +315,7 @@ private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext } } } + + [LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")] + partial void HookReturnedNull(string hookName); + + [LoggerMessage(101, LogLevel.Error, "Error while evaluating flag {FlagKey}")] + partial void FlagEvaluationError(string flagKey, Exception exception); + + [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/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 9509eb8a..7c656cec 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -6,7 +6,6 @@ using AutoFixture; using FluentAssertions; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Internal; using NSubstitute; using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; @@ -182,12 +181,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc _ = mockedFeatureProvider.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); - mockedLogger.Received(1).Log( - LogLevel.Error, - Arg.Any(), - Arg.Is(t => string.Equals($"Error while evaluating flag {flagName}", t.ToString(), StringComparison.InvariantCultureIgnoreCase)), - Arg.Any(), - Arg.Any>()); + mockedLogger.Received(1).IsEnabled(LogLevel.Error); } [Fact] From 11a03332726f07dd0327d222e6bd6e1843db460c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:35:32 -0400 Subject: [PATCH 169/316] chore(deps): update codecov/codecov-action action to v4 (#227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [codecov/codecov-action](https://togithub.com/codecov/codecov-action) | action | major | `v3.1.6` -> `v4.3.0` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v4.3.0`](https://togithub.com/codecov/codecov-action/releases/tag/v4.3.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.2.0...v4.3.0) #### What's Changed - fix: automatically detect if using GitHub enterprise by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1356](https://togithub.com/codecov/codecov-action/pull/1356) - build(deps-dev): bump typescript from 5.4.3 to 5.4.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1355](https://togithub.com/codecov/codecov-action/pull/1355) - build(deps): bump github/codeql-action from 3.24.9 to 3.24.10 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1360](https://togithub.com/codecov/codecov-action/pull/1360) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 7.5.0 to 7.6.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1364](https://togithub.com/codecov/codecov-action/pull/1364) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 7.5.0 to 7.6.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1363](https://togithub.com/codecov/codecov-action/pull/1363) - feat: add network params by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1365](https://togithub.com/codecov/codecov-action/pull/1365) - build(deps): bump undici from 5.28.3 to 5.28.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1361](https://togithub.com/codecov/codecov-action/pull/1361) - chore(release): v4.3.0 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1366](https://togithub.com/codecov/codecov-action/pull/1366) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.2.0...v4.3.0 ### [`v4.2.0`](https://togithub.com/codecov/codecov-action/releases/tag/v4.2.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.1.1...v4.2.0) #### What's Changed - chore(deps): update deps by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1351](https://togithub.com/codecov/codecov-action/pull/1351) - feat: allow for authentication via OIDC token by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1330](https://togithub.com/codecov/codecov-action/pull/1330) - fix: use_oidc shoudl be required false by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1353](https://togithub.com/codecov/codecov-action/pull/1353) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.1.1...v4.2.0 ### [`v4.1.1`](https://togithub.com/codecov/codecov-action/releases/tag/v4.1.1) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.1.0...v4.1.1) #### What's Changed - build(deps): bump github/codeql-action from 3.24.5 to 3.24.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1315](https://togithub.com/codecov/codecov-action/pull/1315) - build(deps-dev): bump typescript from 5.3.3 to 5.4.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1319](https://togithub.com/codecov/codecov-action/pull/1319) - Removed mention of Mercurial by [@​drazisil-codecov](https://togithub.com/drazisil-codecov) in [https://github.com/codecov/codecov-action/pull/1325](https://togithub.com/codecov/codecov-action/pull/1325) - build(deps): bump github/codeql-action from 3.24.6 to 3.24.7 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1332](https://togithub.com/codecov/codecov-action/pull/1332) - build(deps): bump actions/checkout from 4.1.1 to 4.1.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1331](https://togithub.com/codecov/codecov-action/pull/1331) - fix: force version by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1329](https://togithub.com/codecov/codecov-action/pull/1329) - build(deps-dev): bump typescript from 5.4.2 to 5.4.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1334](https://togithub.com/codecov/codecov-action/pull/1334) - build(deps): bump undici from 5.28.2 to 5.28.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1338](https://togithub.com/codecov/codecov-action/pull/1338) - build(deps): bump github/codeql-action from 3.24.7 to 3.24.9 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1341](https://togithub.com/codecov/codecov-action/pull/1341) - fix: typo in disable_safe_directory by [@​mkroening](https://togithub.com/mkroening) in [https://github.com/codecov/codecov-action/pull/1343](https://togithub.com/codecov/codecov-action/pull/1343) - chore(release): 4.1.1 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1344](https://togithub.com/codecov/codecov-action/pull/1344) #### New Contributors - [@​mkroening](https://togithub.com/mkroening) made their first contribution in [https://github.com/codecov/codecov-action/pull/1343](https://togithub.com/codecov/codecov-action/pull/1343) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.1.0...v4.1.1 ### [`v4.1.0`](https://togithub.com/codecov/codecov-action/releases/tag/v4.1.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.0.2...v4.1.0) ##### What's Changed - fix: set safe directory by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1304](https://togithub.com/codecov/codecov-action/pull/1304) - build(deps): bump github/codeql-action from 3.24.3 to 3.24.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1306](https://togithub.com/codecov/codecov-action/pull/1306) - build(deps-dev): bump eslint from 8.56.0 to 8.57.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1305](https://togithub.com/codecov/codecov-action/pull/1305) - chore(release): v4.1.0 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1307](https://togithub.com/codecov/codecov-action/pull/1307) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.0.2...v4.1.0 ### [`v4.0.2`](https://togithub.com/codecov/codecov-action/releases/tag/v4.0.2) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.0.1...v4.0.2) ##### What's Changed - Update README.md by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1251](https://togithub.com/codecov/codecov-action/pull/1251) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.11 to 29.5.12 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1257](https://togithub.com/codecov/codecov-action/pull/1257) - build(deps): bump github/codeql-action from 3.23.2 to 3.24.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1266](https://togithub.com/codecov/codecov-action/pull/1266) - Escape pipes in table of arguments by [@​jwodder](https://togithub.com/jwodder) in [https://github.com/codecov/codecov-action/pull/1265](https://togithub.com/codecov/codecov-action/pull/1265) - Add link to docs on Dependabot secrets by [@​ianlewis](https://togithub.com/ianlewis) in [https://github.com/codecov/codecov-action/pull/1260](https://togithub.com/codecov/codecov-action/pull/1260) - fix: working-directory input for all stages by [@​Bo98](https://togithub.com/Bo98) in [https://github.com/codecov/codecov-action/pull/1272](https://togithub.com/codecov/codecov-action/pull/1272) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.20.0 to 6.21.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1271](https://togithub.com/codecov/codecov-action/pull/1271) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.20.0 to 6.21.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1269](https://togithub.com/codecov/codecov-action/pull/1269) - build(deps): bump github/codeql-action from 3.24.0 to 3.24.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1298](https://togithub.com/codecov/codecov-action/pull/1298) - Use updated syntax for GitHub Markdown notes by [@​jamacku](https://togithub.com/jamacku) in [https://github.com/codecov/codecov-action/pull/1300](https://togithub.com/codecov/codecov-action/pull/1300) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.21.0 to 7.0.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1290](https://togithub.com/codecov/codecov-action/pull/1290) - build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1286](https://togithub.com/codecov/codecov-action/pull/1286) - chore(release): bump to 4.0.2 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1302](https://togithub.com/codecov/codecov-action/pull/1302) ##### New Contributors - [@​jwodder](https://togithub.com/jwodder) made their first contribution in [https://github.com/codecov/codecov-action/pull/1265](https://togithub.com/codecov/codecov-action/pull/1265) - [@​ianlewis](https://togithub.com/ianlewis) made their first contribution in [https://github.com/codecov/codecov-action/pull/1260](https://togithub.com/codecov/codecov-action/pull/1260) - [@​Bo98](https://togithub.com/Bo98) made their first contribution in [https://github.com/codecov/codecov-action/pull/1272](https://togithub.com/codecov/codecov-action/pull/1272) - [@​jamacku](https://togithub.com/jamacku) made their first contribution in [https://github.com/codecov/codecov-action/pull/1300](https://togithub.com/codecov/codecov-action/pull/1300) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.0.1...v4.0.2 ### [`v4.0.1`](https://togithub.com/codecov/codecov-action/releases/tag/v4.0.1) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.0.0...v4.0.1) ##### What's Changed - Update README.md by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1243](https://togithub.com/codecov/codecov-action/pull/1243) - Add all args by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1245](https://togithub.com/codecov/codecov-action/pull/1245) - fix: show both token uses in readme by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1250](https://togithub.com/codecov/codecov-action/pull/1250) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.0.0...v4.0.1 ### [`v4.0.0`](https://togithub.com/codecov/codecov-action/releases/tag/v4.0.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v3.1.6...v4.0.0) v4 of the Codecov Action uses the [CLI](https://docs.codecov.com/docs/the-codecov-cli) as the underlying upload. The CLI has helped to power new features including local upload, the global upload token, and new upcoming features. ##### Breaking Changes - The Codecov Action runs as a `node20` action due to `node16` deprecation. See [this post from GitHub](https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/) on how to migrate. - Tokenless uploading is unsupported. However, PRs made from forks to the upstream public repos will support tokenless (e.g. contributors to OS projects do not need the upstream repo's Codecov token). This [doc](https://docs.codecov.com/docs/adding-the-codecov-token#github-actions) shows instructions on how to add the Codecov token. - OS platforms have been added, though some may not be automatically detected. To see a list of platforms, see our [CLI download page](https://cli.codecov.io) - Various arguments to the Action have been changed. Please be aware that the arguments match with the CLI's needs `v3` versions and below will not have access to CLI features (e.g. global upload token, ATS). ##### What's Changed - build(deps): bump openpgp from 5.8.0 to 5.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/985](https://togithub.com/codecov/codecov-action/pull/985) - build(deps): bump actions/checkout from 3.0.0 to 3.5.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1000](https://togithub.com/codecov/codecov-action/pull/1000) - build(deps): bump ossf/scorecard-action from 2.1.3 to 2.2.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1006](https://togithub.com/codecov/codecov-action/pull/1006) - build(deps): bump tough-cookie from 4.0.0 to 4.1.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1013](https://togithub.com/codecov/codecov-action/pull/1013) - build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1024](https://togithub.com/codecov/codecov-action/pull/1024) - build(deps): bump node-fetch from 3.3.1 to 3.3.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1031](https://togithub.com/codecov/codecov-action/pull/1031) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.1.4 to 20.4.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1032](https://togithub.com/codecov/codecov-action/pull/1032) - build(deps): bump github/codeql-action from 1.0.26 to 2.21.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1033](https://togithub.com/codecov/codecov-action/pull/1033) - build commit,report and upload args based on codecovcli by [@​dana-yaish](https://togithub.com/dana-yaish) in [https://github.com/codecov/codecov-action/pull/943](https://togithub.com/codecov/codecov-action/pull/943) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.4.5 to 20.5.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1055](https://togithub.com/codecov/codecov-action/pull/1055) - build(deps): bump github/codeql-action from 2.21.2 to 2.21.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1051](https://togithub.com/codecov/codecov-action/pull/1051) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.5.3 to 20.5.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1058](https://togithub.com/codecov/codecov-action/pull/1058) - chore(deps): update outdated deps by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1059](https://togithub.com/codecov/codecov-action/pull/1059) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.5.4 to 20.5.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1060](https://togithub.com/codecov/codecov-action/pull/1060) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.4.1 to 6.5.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1065](https://togithub.com/codecov/codecov-action/pull/1065) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.4.1 to 6.5.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1064](https://togithub.com/codecov/codecov-action/pull/1064) - build(deps): bump actions/checkout from 3.5.3 to 3.6.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1063](https://togithub.com/codecov/codecov-action/pull/1063) - build(deps-dev): bump eslint from 8.47.0 to 8.48.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1061](https://togithub.com/codecov/codecov-action/pull/1061) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.5.6 to 20.5.7 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1062](https://togithub.com/codecov/codecov-action/pull/1062) - build(deps): bump openpgp from 5.9.0 to 5.10.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1066](https://togithub.com/codecov/codecov-action/pull/1066) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.5.7 to 20.5.9 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1070](https://togithub.com/codecov/codecov-action/pull/1070) - build(deps): bump github/codeql-action from 2.21.4 to 2.21.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1069](https://togithub.com/codecov/codecov-action/pull/1069) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.5.0 to 6.6.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1072](https://togithub.com/codecov/codecov-action/pull/1072) - Update README.md by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1073](https://togithub.com/codecov/codecov-action/pull/1073) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.5.0 to 6.6.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1071](https://togithub.com/codecov/codecov-action/pull/1071) - build(deps-dev): bump [@​vercel/ncc](https://togithub.com/vercel/ncc) from 0.36.1 to 0.38.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1074](https://togithub.com/codecov/codecov-action/pull/1074) - build(deps): bump [@​actions/core](https://togithub.com/actions/core) from 1.10.0 to 1.10.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1081](https://togithub.com/codecov/codecov-action/pull/1081) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.6.0 to 6.7.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1080](https://togithub.com/codecov/codecov-action/pull/1080) - build(deps): bump actions/checkout from 3.6.0 to 4.0.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1078](https://togithub.com/codecov/codecov-action/pull/1078) - build(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1077](https://togithub.com/codecov/codecov-action/pull/1077) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.5.9 to 20.6.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1075](https://togithub.com/codecov/codecov-action/pull/1075) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.6.0 to 6.7.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1079](https://togithub.com/codecov/codecov-action/pull/1079) - build(deps-dev): bump eslint from 8.48.0 to 8.49.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1076](https://togithub.com/codecov/codecov-action/pull/1076) - use cli instead of node uploader by [@​dana-yaish](https://togithub.com/dana-yaish) in [https://github.com/codecov/codecov-action/pull/1068](https://togithub.com/codecov/codecov-action/pull/1068) - chore(release): 4.0.0-beta.1 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1084](https://togithub.com/codecov/codecov-action/pull/1084) - not adding -n if empty to do-upload command by [@​dana-yaish](https://togithub.com/dana-yaish) in [https://github.com/codecov/codecov-action/pull/1085](https://togithub.com/codecov/codecov-action/pull/1085) - 4.0.0-beta.2 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1086](https://togithub.com/codecov/codecov-action/pull/1086) - build(deps-dev): bump jest from 29.6.4 to 29.7.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1082](https://togithub.com/codecov/codecov-action/pull/1082) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.4 to 29.5.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1092](https://togithub.com/codecov/codecov-action/pull/1092) - build(deps): bump github/codeql-action from 2.21.5 to 2.21.7 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1094](https://togithub.com/codecov/codecov-action/pull/1094) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.6.0 to 20.6.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1093](https://togithub.com/codecov/codecov-action/pull/1093) - build(deps): bump openpgp from 5.10.1 to 5.10.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1096](https://togithub.com/codecov/codecov-action/pull/1096) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.7.0 to 6.7.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1095](https://togithub.com/codecov/codecov-action/pull/1095) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.6.2 to 20.6.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1098](https://togithub.com/codecov/codecov-action/pull/1098) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.7.0 to 6.7.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1097](https://togithub.com/codecov/codecov-action/pull/1097) - feat: add plugins by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1099](https://togithub.com/codecov/codecov-action/pull/1099) - build(deps-dev): bump eslint from 8.49.0 to 8.50.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1104](https://togithub.com/codecov/codecov-action/pull/1104) - build(deps): bump github/codeql-action from 2.21.7 to 2.21.8 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1102](https://togithub.com/codecov/codecov-action/pull/1102) - build(deps): bump actions/checkout from 4.0.0 to 4.1.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1101](https://togithub.com/codecov/codecov-action/pull/1101) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.7.2 to 6.7.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1108](https://togithub.com/codecov/codecov-action/pull/1108) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.6.3 to 20.7.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1107](https://togithub.com/codecov/codecov-action/pull/1107) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.7.2 to 6.7.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1106](https://togithub.com/codecov/codecov-action/pull/1106) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.7.0 to 20.7.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1111](https://togithub.com/codecov/codecov-action/pull/1111) - build(deps): bump github/codeql-action from 2.21.8 to 2.21.9 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1113](https://togithub.com/codecov/codecov-action/pull/1113) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.7.1 to 20.8.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1112](https://togithub.com/codecov/codecov-action/pull/1112) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.8.0 to 20.8.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1114](https://togithub.com/codecov/codecov-action/pull/1114) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.7.3 to 6.7.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1115](https://togithub.com/codecov/codecov-action/pull/1115) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.7.4 to 6.7.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1123](https://togithub.com/codecov/codecov-action/pull/1123) - build(deps): bump ossf/scorecard-action from 2.2.0 to 2.3.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1120](https://togithub.com/codecov/codecov-action/pull/1120) - build(deps): bump github/codeql-action from 2.21.9 to 2.22.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1119](https://togithub.com/codecov/codecov-action/pull/1119) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.7.3 to 6.7.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1122](https://togithub.com/codecov/codecov-action/pull/1122) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.8.2 to 20.8.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1121](https://togithub.com/codecov/codecov-action/pull/1121) - build(deps-dev): bump eslint from 8.50.0 to 8.51.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1117](https://togithub.com/codecov/codecov-action/pull/1117) - build(deps): bump [@​actions/github](https://togithub.com/actions/github) from 5.1.1 to 6.0.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1124](https://togithub.com/codecov/codecov-action/pull/1124) - build(deps): bump github/codeql-action from 2.22.0 to 2.22.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1127](https://togithub.com/codecov/codecov-action/pull/1127) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.8.4 to 20.8.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1126](https://togithub.com/codecov/codecov-action/pull/1126) - build(deps-dev): bump [@​babel/traverse](https://togithub.com/babel/traverse) from 7.22.11 to 7.23.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1129](https://togithub.com/codecov/codecov-action/pull/1129) - build(deps): bump undici from 5.25.4 to 5.26.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1128](https://togithub.com/codecov/codecov-action/pull/1128) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.7.5 to 6.8.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1130](https://togithub.com/codecov/codecov-action/pull/1130) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.7.5 to 6.8.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1131](https://togithub.com/codecov/codecov-action/pull/1131) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.8.6 to 20.8.7 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1135](https://togithub.com/codecov/codecov-action/pull/1135) - build(deps-dev): bump [@​vercel/ncc](https://togithub.com/vercel/ncc) from 0.38.0 to 0.38.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1136](https://togithub.com/codecov/codecov-action/pull/1136) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.5 to 29.5.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1137](https://togithub.com/codecov/codecov-action/pull/1137) - build(deps): bump github/codeql-action from 2.22.3 to 2.22.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1142](https://togithub.com/codecov/codecov-action/pull/1142) - build(deps): bump actions/checkout from 4.1.0 to 4.1.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1141](https://togithub.com/codecov/codecov-action/pull/1141) - build(deps-dev): bump eslint from 8.51.0 to 8.52.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1140](https://togithub.com/codecov/codecov-action/pull/1140) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.8.0 to 6.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1147](https://togithub.com/codecov/codecov-action/pull/1147) - build(deps-dev): bump [@​types/node](https://togithub.com/types/node) from 20.8.7 to 20.8.8 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1146](https://togithub.com/codecov/codecov-action/pull/1146) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.8.0 to 6.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1145](https://togithub.com/codecov/codecov-action/pull/1145) - chore(deps): move from node-fetch to undici by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1148](https://togithub.com/codecov/codecov-action/pull/1148) - build(deps): bump openpgp from 5.10.2 to 5.11.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1149](https://togithub.com/codecov/codecov-action/pull/1149) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.9.0 to 6.9.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1155](https://togithub.com/codecov/codecov-action/pull/1155) - build(deps): bump github/codeql-action from 2.22.4 to 2.22.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1152](https://togithub.com/codecov/codecov-action/pull/1152) - build(deps): bump ossf/scorecard-action from 2.3.0 to 2.3.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1151](https://togithub.com/codecov/codecov-action/pull/1151) - build(deps): bump undici from 5.26.5 to 5.27.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1150](https://togithub.com/codecov/codecov-action/pull/1150) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.6 to 29.5.7 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1153](https://togithub.com/codecov/codecov-action/pull/1153) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.9.0 to 6.9.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1154](https://togithub.com/codecov/codecov-action/pull/1154) - build(deps): bump undici from 5.27.0 to 5.27.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1157](https://togithub.com/codecov/codecov-action/pull/1157) - build(deps-dev): bump eslint from 8.52.0 to 8.53.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1156](https://togithub.com/codecov/codecov-action/pull/1156) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.9.1 to 6.10.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1159](https://togithub.com/codecov/codecov-action/pull/1159) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.9.1 to 6.10.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1158](https://togithub.com/codecov/codecov-action/pull/1158) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.7 to 29.5.8 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1161](https://togithub.com/codecov/codecov-action/pull/1161) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.10.0 to 6.11.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1164](https://togithub.com/codecov/codecov-action/pull/1164) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.10.0 to 6.11.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1163](https://togithub.com/codecov/codecov-action/pull/1163) - build(deps): bump github/codeql-action from 2.22.5 to 2.22.7 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1167](https://togithub.com/codecov/codecov-action/pull/1167) - build(deps-dev): bump eslint from 8.53.0 to 8.54.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1166](https://togithub.com/codecov/codecov-action/pull/1166) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.8 to 29.5.9 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1172](https://togithub.com/codecov/codecov-action/pull/1172) - build(deps-dev): bump typescript from 5.2.2 to 5.3.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1171](https://togithub.com/codecov/codecov-action/pull/1171) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.11.0 to 6.12.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1170](https://togithub.com/codecov/codecov-action/pull/1170) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.11.0 to 6.12.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1169](https://togithub.com/codecov/codecov-action/pull/1169) - build(deps): bump github/codeql-action from 2.22.7 to 2.22.8 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1175](https://togithub.com/codecov/codecov-action/pull/1175) - build(deps): bump undici from 5.27.2 to 5.28.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1174](https://togithub.com/codecov/codecov-action/pull/1174) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.9 to 29.5.10 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1173](https://togithub.com/codecov/codecov-action/pull/1173) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.12.0 to 6.13.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1178](https://togithub.com/codecov/codecov-action/pull/1178) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.12.0 to 6.13.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1180](https://togithub.com/codecov/codecov-action/pull/1180) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.13.0 to 6.13.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1181](https://togithub.com/codecov/codecov-action/pull/1181) - build(deps): bump undici from 5.28.0 to 5.28.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1179](https://togithub.com/codecov/codecov-action/pull/1179) - build(deps-dev): bump eslint from 8.54.0 to 8.55.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1183](https://togithub.com/codecov/codecov-action/pull/1183) - build(deps): bump undici from 5.28.1 to 5.28.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1182](https://togithub.com/codecov/codecov-action/pull/1182) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.13.1 to 6.13.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1185](https://togithub.com/codecov/codecov-action/pull/1185) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.13.1 to 6.13.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1184](https://togithub.com/codecov/codecov-action/pull/1184) - build(deps-dev): bump [@​types/jest](https://togithub.com/types/jest) from 29.5.10 to 29.5.11 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1187](https://togithub.com/codecov/codecov-action/pull/1187) - build(deps): bump undici from 5.28.2 to 6.0.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1186](https://togithub.com/codecov/codecov-action/pull/1186) - build(deps-dev): bump typescript from 5.3.2 to 5.3.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1189](https://togithub.com/codecov/codecov-action/pull/1189) - build(deps): bump undici from 6.0.0 to 6.0.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1188](https://togithub.com/codecov/codecov-action/pull/1188) - build(deps): bump github/codeql-action from 2.22.8 to 2.22.9 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1191](https://togithub.com/codecov/codecov-action/pull/1191) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.13.2 to 6.14.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1193](https://togithub.com/codecov/codecov-action/pull/1193) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.13.2 to 6.14.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1192](https://togithub.com/codecov/codecov-action/pull/1192) - build(deps-dev): bump eslint from 8.55.0 to 8.56.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1194](https://togithub.com/codecov/codecov-action/pull/1194) - build(deps): bump github/codeql-action from 2.22.9 to 3.22.11 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1195](https://togithub.com/codecov/codecov-action/pull/1195) - build(deps): bump actions/upload-artifact from 3.1.3 to 4.0.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1196](https://togithub.com/codecov/codecov-action/pull/1196) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.14.0 to 6.15.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1198](https://togithub.com/codecov/codecov-action/pull/1198) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.14.0 to 6.15.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1197](https://togithub.com/codecov/codecov-action/pull/1197) - build(deps): bump undici from 6.0.1 to 6.2.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1199](https://togithub.com/codecov/codecov-action/pull/1199) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.15.0 to 6.17.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1206](https://togithub.com/codecov/codecov-action/pull/1206) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.15.0 to 6.17.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1205](https://togithub.com/codecov/codecov-action/pull/1205) - build(deps): bump undici from 6.2.0 to 6.2.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1201](https://togithub.com/codecov/codecov-action/pull/1201) - build(deps): bump github/codeql-action from 3.22.11 to 3.22.12 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1200](https://togithub.com/codecov/codecov-action/pull/1200) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.17.0 to 6.18.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1208](https://togithub.com/codecov/codecov-action/pull/1208) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.17.0 to 6.18.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1207](https://togithub.com/codecov/codecov-action/pull/1207) - build(deps): bump undici from 6.2.1 to 6.3.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1211](https://togithub.com/codecov/codecov-action/pull/1211) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.18.0 to 6.18.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1210](https://togithub.com/codecov/codecov-action/pull/1210) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.18.0 to 6.18.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1209](https://togithub.com/codecov/codecov-action/pull/1209) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.18.1 to 6.19.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1215](https://togithub.com/codecov/codecov-action/pull/1215) - build(deps): bump github/codeql-action from 3.22.12 to 3.23.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1213](https://togithub.com/codecov/codecov-action/pull/1213) - build(deps): bump actions/upload-artifact from 4.0.0 to 4.1.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1212](https://togithub.com/codecov/codecov-action/pull/1212) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.18.1 to 6.19.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1214](https://togithub.com/codecov/codecov-action/pull/1214) - fix: downgrade undici as it has a breaking change by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1219](https://togithub.com/codecov/codecov-action/pull/1219) - fix: remove openpgp dep due to licensing and use gpg by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1218](https://togithub.com/codecov/codecov-action/pull/1218) - chore(ci): add fossa workflow by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1216](https://togithub.com/codecov/codecov-action/pull/1216) - build(deps): bump actions/upload-artifact from 4.1.0 to 4.2.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1222](https://togithub.com/codecov/codecov-action/pull/1222) - build(deps): bump github/codeql-action from 3.23.0 to 3.23.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1221](https://togithub.com/codecov/codecov-action/pull/1221) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.19.0 to 6.19.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1225](https://togithub.com/codecov/codecov-action/pull/1225) - build(deps-dev): bump ts-jest from 29.1.1 to 29.1.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1224](https://togithub.com/codecov/codecov-action/pull/1224) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.19.0 to 6.19.1 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1223](https://togithub.com/codecov/codecov-action/pull/1223) - build(deps): bump actions/upload-artifact from 4.2.0 to 4.3.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1232](https://togithub.com/codecov/codecov-action/pull/1232) - build(deps): bump github/codeql-action from 3.23.1 to 3.23.2 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1231](https://togithub.com/codecov/codecov-action/pull/1231) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 6.19.1 to 6.20.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1235](https://togithub.com/codecov/codecov-action/pull/1235) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 6.19.1 to 6.20.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1234](https://togithub.com/codecov/codecov-action/pull/1234) - chore(ci): bump to node20 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1236](https://togithub.com/codecov/codecov-action/pull/1236) - Update README.md by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1237](https://togithub.com/codecov/codecov-action/pull/1237) - Update package.json by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1238](https://togithub.com/codecov/codecov-action/pull/1238) - fix: allow for other archs by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1239](https://togithub.com/codecov/codecov-action/pull/1239) - fix: update action.yml by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1240](https://togithub.com/codecov/codecov-action/pull/1240) ##### New Contributors - [@​dana-yaish](https://togithub.com/dana-yaish) made their first contribution in [https://github.com/codecov/codecov-action/pull/943](https://togithub.com/codecov/codecov-action/pull/943) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v3.1.6...v4.0.0
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@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> --- .github/workflows/code-coverage.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 32f57883..fc665476 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -31,13 +31,15 @@ jobs: dotnet-version: | 6.0.x 7.0.x + 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v3.1.6 + - uses: codecov/codecov-action@v4.3.0 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true verbose: true + token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} From bc8301d1c54e0b48ede3235877d969f28d61fb29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:57:28 +0100 Subject: [PATCH 170/316] chore(deps): update dotnet monorepo (#218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | Type | Update | |---|---|---|---|---|---|---|---| | [Microsoft.Extensions.Logging.Abstractions](https://dot.net/) ([source](https://togithub.com/dotnet/runtime)) | `8.0.0` -> `8.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.0/8.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.0/8.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [dotnet-sdk](https://togithub.com/dotnet/sdk) | `8.0.100` -> `8.0.203` | [![age](https://developer.mend.io/api/mc/badges/age/dotnet-version/dotnet-sdk/8.0.203?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/dotnet-version/dotnet-sdk/8.0.203?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/dotnet-version/dotnet-sdk/8.0.100/8.0.203?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/dotnet-version/dotnet-sdk/8.0.100/8.0.203?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dotnet-sdk | patch | --- ### Release Notes
dotnet/runtime (Microsoft.Extensions.Logging.Abstractions) ### [`v8.0.1`](https://togithub.com/dotnet/runtime/releases/tag/v8.0.1): .NET 8.0.1 [Release](https://togithub.com/dotnet/core/releases/tag/v8.0.1)
dotnet/sdk (dotnet-sdk) ### [`v8.0.203`](https://togithub.com/dotnet/sdk/compare/v8.0.202...v8.0.203) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.202...v8.0.203) ### [`v8.0.202`](https://togithub.com/dotnet/sdk/releases/tag/v8.0.202): .NET 8.0.3 [Release](https://togithub.com/dotnet/core/releases/tag/v8.0.3) ### [`v8.0.200`](https://togithub.com/dotnet/sdk/releases/tag/v8.0.200): .NET 8.0.2 [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.103...v8.0.200) [Release](https://togithub.com/dotnet/core/releases/tag/v8.0.2) ### [`v8.0.103`](https://togithub.com/dotnet/sdk/compare/v8.0.102...v8.0.103) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.102...v8.0.103) ### [`v8.0.102`](https://togithub.com/dotnet/sdk/compare/v8.0.101...v8.0.102) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.101...v8.0.102) ### [`v8.0.101`](https://togithub.com/dotnet/sdk/releases/tag/v8.0.101): .NET 8.0.1 [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.100...v8.0.101) [Release](https://togithub.com/dotnet/core/releases/tag/v8.0.1)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@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> --- .github/workflows/ci.yml | 124 ++++++++++++++-------------- .github/workflows/code-coverage.yml | 10 +-- .github/workflows/e2e.yml | 37 +++++---- .github/workflows/release.yml | 1 + Directory.Packages.props | 2 +- global.json | 2 +- 6 files changed, 90 insertions(+), 86 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 591c1582..27fb5043 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,30 +19,31 @@ jobs: runs-on: ${{ matrix.os }} steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 7.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json - - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build --no-restore - - - name: Test - run: dotnet test --no-build --logger GitHubActions + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore + + - name: Test + run: dotnet test --no-build --logger GitHubActions packaging: needs: build @@ -54,40 +55,41 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 7.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json - - - name: Restore - run: dotnet restore - - - name: Pack NuGet packages (CI versions) - if: startsWith(github.ref, 'refs/heads/') - run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" - - - name: Pack NuGet packages (PR versions) - if: startsWith(github.ref, 'refs/pull/') - run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" - - - name: Publish NuGet packages (base) - if: github.event.pull_request.head.repo.fork == false - run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json - - - name: Publish NuGet packages (fork) - if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.1 - with: - name: nupkgs - path: src/**/*.nupkg + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json + + - name: Restore + run: dotnet restore + + - name: Pack NuGet packages (CI versions) + if: startsWith(github.ref, 'refs/heads/') + run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + + - name: Pack NuGet packages (PR versions) + if: startsWith(github.ref, 'refs/pull/') + run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + + - name: Publish NuGet packages (base) + if: github.event.pull_request.head.repo.fork == false + run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json + + - name: Publish NuGet packages (fork) + if: github.event.pull_request.head.repo.fork == true + uses: actions/upload-artifact@v4.3.1 + with: + name: nupkgs + path: src/**/*.nupkg diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index fc665476..b681d66d 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -19,9 +19,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 @@ -34,8 +34,8 @@ jobs: 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - - name: Run Test - run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + - name: Run Test + run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - uses: codecov/codecov-action@v4.3.0 with: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4dea1592..2cc0a84f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -14,24 +14,25 @@ jobs: e2e-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 7.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json - - name: Initialize Tests - run: | - git submodule update --init --recursive - cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ + - name: Initialize Tests + run: | + git submodule update --init --recursive + cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ - - name: Run Tests - run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions + - name: Run Tests + run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 899c3049..3d8aa265 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: dotnet-version: | 6.0.x 7.0.x + 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install dependencies diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b67b6aa..bd7b52df 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/global.json b/global.json index 0aca8b12..8d00746b 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.100" + "version": "8.0.203" } } From d2f08cde4ca3cbaa53f4236bccbc7b82657d499a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:19:04 +0100 Subject: [PATCH 171/316] ci: Change dotnet formatter (#260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR - This PR changes the code formatter we currently use to the new built-in in the SDK. See https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format ### Notes - We should now use this tool since it is now shipped as part of the dotnet SDK, removing the need to install an external one. - As pointed out in the chat, this build runs in parallel and is relatively fast. I will enable all PRs instead of those focusing on changes to .cs files. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Todd Baert --- .github/workflows/dotnet-format.yml | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 3455a1e0..9af0ae8b 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -2,29 +2,22 @@ name: dotnet format on: push: - branches: [ main ] - paths: - - '**.cs' - - '.editorconfig' + branches: [main] pull_request: - branches: [ main ] - paths: - - '**.cs' - - '.editorconfig' + branches: [main] jobs: check-format: runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v4 + - name: Check out code + uses: actions/checkout@v4 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x - - name: Install format tool - run: dotnet tool install -g dotnet-format - - - name: dotnet format - run: dotnet-format --folder --check + - name: dotnet format + run: dotnet format --verify-no-changes OpenFeature.sln From 43f14cca072372ecacec89a949c85f763c1ee7b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:58:24 -0400 Subject: [PATCH 172/316] chore(deps): update xunit-dotnet monorepo (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [xunit](https://togithub.com/xunit/xunit) | `2.7.0` -> `2.7.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [xunit.runner.visualstudio](https://togithub.com/xunit/visualstudio.xunit) | `2.5.7` -> `2.5.8` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit.runner.visualstudio/2.5.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit.runner.visualstudio/2.5.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit.runner.visualstudio/2.5.7/2.5.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit.runner.visualstudio/2.5.7/2.5.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
xunit/xunit (xunit) ### [`v2.7.1`](https://togithub.com/xunit/xunit/compare/2.7.0...2.7.1) [Compare Source](https://togithub.com/xunit/xunit/compare/2.7.0...2.7.1)
xunit/visualstudio.xunit (xunit.runner.visualstudio) ### [`v2.5.8`](https://togithub.com/xunit/visualstudio.xunit/compare/2.5.7...2.5.8) [Compare Source](https://togithub.com/xunit/visualstudio.xunit/compare/2.5.7...2.5.8)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bd7b52df..14e21a27 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,8 +24,8 @@ - - + +
From 77186495cd3d567b0aabd418f23a65567656b54d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:06:24 -0700 Subject: [PATCH 173/316] chore(deps): update actions/upload-artifact action to v4.3.3 (#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/upload-artifact](https://togithub.com/actions/upload-artifact) | action | patch | `v4.3.1` -> `v4.3.3` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.3.3`](https://togithub.com/actions/upload-artifact/releases/tag/v4.3.3) [Compare Source](https://togithub.com/actions/upload-artifact/compare/v4.3.2...v4.3.3) ##### What's Changed - updating `@actions/artifact` dependency to v2.1.6 by [@​eggyhead](https://togithub.com/eggyhead) in [https://github.com/actions/upload-artifact/pull/565](https://togithub.com/actions/upload-artifact/pull/565) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4.3.2...v4.3.3 ### [`v4.3.2`](https://togithub.com/actions/upload-artifact/releases/tag/v4.3.2) [Compare Source](https://togithub.com/actions/upload-artifact/compare/v4.3.1...v4.3.2) #### What's Changed - Update release-new-action-version.yml by [@​konradpabjan](https://togithub.com/konradpabjan) in [https://github.com/actions/upload-artifact/pull/516](https://togithub.com/actions/upload-artifact/pull/516) - Minor fix to the migration readme by [@​andrewakim](https://togithub.com/andrewakim) in [https://github.com/actions/upload-artifact/pull/523](https://togithub.com/actions/upload-artifact/pull/523) - Update readme with v3/v2/v1 deprecation notice by [@​robherley](https://togithub.com/robherley) in [https://github.com/actions/upload-artifact/pull/561](https://togithub.com/actions/upload-artifact/pull/561) - updating `@actions/artifact` dependency to v2.1.5 and `@actions/core` to v1.0.1 by [@​eggyhead](https://togithub.com/eggyhead) in [https://github.com/actions/upload-artifact/pull/562](https://togithub.com/actions/upload-artifact/pull/562) #### New Contributors - [@​andrewakim](https://togithub.com/andrewakim) made their first contribution in [https://github.com/actions/upload-artifact/pull/523](https://togithub.com/actions/upload-artifact/pull/523) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4.3.1...v4.3.2
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27fb5043..893834b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: nupkgs path: src/**/*.nupkg From ac7d7debf50cef08668bcd9457d3f830b8718806 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Wed, 24 Apr 2024 02:24:44 +1000 Subject: [PATCH 174/316] feat!: Use same type for flag metadata and event metadata (#241) ## This PR **BREAKING CHANGE**, merge with 2.0 The spec describes two types(flag metadata, and event metadata) that are functionally the same. This PR makes a breaking change to bring both of the types to use a generic ImmutableMetadata type. - Rename BaseMetadata to ImmutableMetadata, make sealed class and implement a empty constructor ### Related Issues Fixes: #234 --------- Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Signed-off-by: Todd Baert Co-authored-by: Todd Baert --- .../Model/FlagEvaluationDetails.cs | 56 +++++++++---------- src/OpenFeature/Model/FlagMetadata.cs | 25 --------- .../{BaseMetadata.cs => ImmutableMetadata.cs} | 29 +++++++--- src/OpenFeature/Model/ProviderEvents.cs | 3 +- src/OpenFeature/Model/ResolutionDetails.cs | 56 +++++++++---------- ...tadataTest.cs => ImmutableMetadataTest.cs} | 26 ++++----- .../OpenFeatureEventTests.cs | 7 +-- 7 files changed, 93 insertions(+), 109 deletions(-) delete mode 100644 src/OpenFeature/Model/FlagMetadata.cs rename src/OpenFeature/Model/{BaseMetadata.cs => ImmutableMetadata.cs} (67%) rename test/OpenFeature.Tests/{FlagMetadataTest.cs => ImmutableMetadataTest.cs} (91%) diff --git a/src/OpenFeature/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs index 9af2f4bf..11283b4f 100644 --- a/src/OpenFeature/Model/FlagEvaluationDetails.cs +++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs @@ -1,29 +1,29 @@ using OpenFeature.Constant; 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 - { + public sealed class FlagEvaluationDetails + { /// /// Feature flag evaluated value /// - public T Value { get; } - + public T Value { get; } + /// /// Feature flag key /// - public string FlagKey { get; } - + public string FlagKey { get; } + /// /// Error that occurred during evaluation /// - public ErrorType ErrorType { get; } - + public ErrorType ErrorType { get; } + /// /// Message containing additional details about an error. /// @@ -31,25 +31,25 @@ public sealed class FlagEvaluationDetails /// details. /// /// - public string? ErrorMessage { get; } - + public string? ErrorMessage { get; } + /// /// Describes the reason for the outcome of the evaluation process /// - public string? Reason { get; } - + 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; } - + 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 FlagMetadata? FlagMetadata { get; } - + public ImmutableMetadata? FlagMetadata { get; } + /// /// Initializes a new instance of the class. /// @@ -60,16 +60,16 @@ public sealed class FlagEvaluationDetails /// Variant /// Error message /// Flag metadata - public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string? reason, string? variant, - string? errorMessage = null, FlagMetadata? flagMetadata = null) - { - this.Value = value; - this.FlagKey = flagKey; - this.ErrorType = errorType; - this.Reason = reason; - this.Variant = variant; - this.ErrorMessage = errorMessage; - this.FlagMetadata = flagMetadata; - } + 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/FlagMetadata.cs b/src/OpenFeature/Model/FlagMetadata.cs deleted file mode 100644 index 0fddbdd3..00000000 --- a/src/OpenFeature/Model/FlagMetadata.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; - -namespace OpenFeature.Model; - -/// -/// Represents the metadata associated with a feature flag. -/// -/// -public sealed class FlagMetadata : BaseMetadata -{ - /// - /// Constructor for the class. - /// - public FlagMetadata() : this([]) - { - } - - /// - /// Constructor for the class. - /// - /// The dictionary containing the metadata. - public FlagMetadata(Dictionary metadata) : base(metadata) - { - } -} diff --git a/src/OpenFeature/Model/BaseMetadata.cs b/src/OpenFeature/Model/ImmutableMetadata.cs similarity index 67% rename from src/OpenFeature/Model/BaseMetadata.cs rename to src/OpenFeature/Model/ImmutableMetadata.cs index 876247df..40d452d0 100644 --- a/src/OpenFeature/Model/BaseMetadata.cs +++ b/src/OpenFeature/Model/ImmutableMetadata.cs @@ -1,16 +1,31 @@ using System.Collections.Generic; using System.Collections.Immutable; +#nullable enable namespace OpenFeature.Model; /// -/// Represents the base class for metadata objects. +/// Represents immutable metadata associated with feature flags and events. /// -public abstract class BaseMetadata +/// +/// +public sealed class ImmutableMetadata { private readonly ImmutableDictionary _metadata; - internal BaseMetadata(Dictionary metadata) + /// + /// Constructor for the class. + /// + public ImmutableMetadata() + { + this._metadata = ImmutableDictionary.Empty; + } + + /// + /// Constructor for the class. + /// + /// The dictionary containing the metadata. + public ImmutableMetadata(Dictionary metadata) { this._metadata = metadata.ToImmutableDictionary(); } @@ -20,7 +35,7 @@ internal BaseMetadata(Dictionary metadata) /// /// The key of the value to retrieve. /// The boolean value associated with the key, or null if the key is not found. - public virtual bool? GetBool(string key) + public bool? GetBool(string key) { return this.GetValue(key); } @@ -30,7 +45,7 @@ internal BaseMetadata(Dictionary metadata) /// /// The key of the value to retrieve. /// The integer value associated with the key, or null if the key is not found. - public virtual int? GetInt(string key) + public int? GetInt(string key) { return this.GetValue(key); } @@ -40,7 +55,7 @@ internal BaseMetadata(Dictionary metadata) /// /// The key of the value to retrieve. /// The double value associated with the key, or null if the key is not found. - public virtual double? GetDouble(string key) + public double? GetDouble(string key) { return this.GetValue(key); } @@ -50,7 +65,7 @@ internal BaseMetadata(Dictionary metadata) /// /// The key of the value to retrieve. /// The string value associated with the key, or null if the key is not found. - public virtual string? GetString(string key) + public string? GetString(string key) { var hasValue = this._metadata.TryGetValue(key, out var value); if (!hasValue) diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index 6feccfb0..5c48fc19 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -36,7 +36,6 @@ public class ProviderEventPayload /// /// Metadata information for the event. /// - // TODO: This needs to be changed to a EventMetadata object - public Dictionary? EventMetadata { get; set; } + public ImmutableMetadata? EventMetadata { get; set; } } } diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs index 5f686d47..78b907d2 100644 --- a/src/OpenFeature/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -1,54 +1,54 @@ using OpenFeature.Constant; namespace OpenFeature.Model -{ +{ /// /// Defines the contract that the is required to return /// Describes the details of the feature flag being evaluated /// /// Flag value type /// - public sealed class ResolutionDetails - { + public sealed class ResolutionDetails + { /// /// Feature flag evaluated value /// - public T Value { get; } - + public T Value { get; } + /// /// Feature flag key /// - public string FlagKey { get; } - + public string FlagKey { get; } + /// /// Error that occurred during evaluation /// /// - public ErrorType ErrorType { get; } - + public ErrorType ErrorType { get; } + /// /// Message containing additional details about an error. /// - public string? ErrorMessage { get; } - + public string? ErrorMessage { get; } + /// /// Describes the reason for the outcome of the evaluation process /// /// - public string? Reason { get; } - + 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; } - + 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 FlagMetadata? FlagMetadata { get; } - + public ImmutableMetadata? FlagMetadata { get; } + /// /// Initializes a new instance of the class. /// @@ -59,16 +59,16 @@ public sealed class ResolutionDetails /// Variant /// Error message /// Flag metadata - public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string? reason = null, - string? variant = null, string? errorMessage = null, FlagMetadata? flagMetadata = null) - { - this.Value = value; - this.FlagKey = flagKey; - this.ErrorType = errorType; - this.Reason = reason; - this.Variant = variant; - this.ErrorMessage = errorMessage; - this.FlagMetadata = flagMetadata; - } + public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string? reason = null, + string? variant = null, 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/test/OpenFeature.Tests/FlagMetadataTest.cs b/test/OpenFeature.Tests/ImmutableMetadataTest.cs similarity index 91% rename from test/OpenFeature.Tests/FlagMetadataTest.cs rename to test/OpenFeature.Tests/ImmutableMetadataTest.cs index d716d91e..344392b0 100644 --- a/test/OpenFeature.Tests/FlagMetadataTest.cs +++ b/test/OpenFeature.Tests/ImmutableMetadataTest.cs @@ -5,7 +5,7 @@ namespace OpenFeature.Tests; -public class FlagMetadataTest +public class ImmutableMetadataTest { [Fact] [Specification("1.4.14", @@ -13,7 +13,7 @@ public class FlagMetadataTest public void GetBool_Should_Return_Null_If_Key_Not_Found() { // Arrange - var flagMetadata = new FlagMetadata(); + var flagMetadata = new ImmutableMetadata(); // Act var result = flagMetadata.GetBool("nonexistentKey"); @@ -35,7 +35,7 @@ public void GetBool_Should_Return_Value_If_Key_Found() "boolKey", true } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetBool("boolKey"); @@ -56,7 +56,7 @@ public void GetBool_Should_Throw_Value_Is_Invalid() "wrongKey", "11a" } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetBool("wrongKey"); @@ -71,7 +71,7 @@ public void GetBool_Should_Throw_Value_Is_Invalid() public void GetInt_Should_Return_Null_If_Key_Not_Found() { // Arrange - var flagMetadata = new FlagMetadata(); + var flagMetadata = new ImmutableMetadata(); // Act var result = flagMetadata.GetInt("nonexistentKey"); @@ -93,7 +93,7 @@ public void GetInt_Should_Return_Value_If_Key_Found() "intKey", 1 } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetInt("intKey"); @@ -115,7 +115,7 @@ public void GetInt_Should_Throw_Value_Is_Invalid() "wrongKey", "11a" } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetInt("wrongKey"); @@ -130,7 +130,7 @@ public void GetInt_Should_Throw_Value_Is_Invalid() public void GetDouble_Should_Return_Null_If_Key_Not_Found() { // Arrange - var flagMetadata = new FlagMetadata(); + var flagMetadata = new ImmutableMetadata(); // Act var result = flagMetadata.GetDouble("nonexistentKey"); @@ -152,7 +152,7 @@ public void GetDouble_Should_Return_Value_If_Key_Found() "doubleKey", 1.2 } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetDouble("doubleKey"); @@ -174,7 +174,7 @@ public void GetDouble_Should_Throw_Value_Is_Invalid() "wrongKey", "11a" } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetDouble("wrongKey"); @@ -189,7 +189,7 @@ public void GetDouble_Should_Throw_Value_Is_Invalid() public void GetString_Should_Return_Null_If_Key_Not_Found() { // Arrange - var flagMetadata = new FlagMetadata(); + var flagMetadata = new ImmutableMetadata(); // Act var result = flagMetadata.GetString("nonexistentKey"); @@ -211,7 +211,7 @@ public void GetString_Should_Return_Value_If_Key_Found() "stringKey", "11" } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetString("stringKey"); @@ -233,7 +233,7 @@ public void GetString_Should_Throw_Value_Is_Invalid() "wrongKey", new object() } }; - var flagMetadata = new FlagMetadata(metadata); + var flagMetadata = new ImmutableMetadata(metadata); // Act var result = flagMetadata.GetString("wrongKey"); diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 599cea30..3a373c98 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -25,12 +25,7 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); - var eventMetadata = new Dictionary - { - { - "foo", "bar" - } - }; + var eventMetadata = new ImmutableMetadata(new Dictionary { { "foo", "bar" } }); var myEvent = new Event { EventPayload = new ProviderEventPayload From 8f8264520814a42b7ed2af8f70340e7673259b6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:30:12 -0700 Subject: [PATCH 175/316] chore(deps): update dependency dotnet-sdk to v8.0.204 (#261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dotnet-sdk](https://togithub.com/dotnet/sdk) | dotnet-sdk | patch | `8.0.203` -> `8.0.204` | --- ### Release Notes
dotnet/sdk (dotnet-sdk) ### [`v8.0.204`](https://togithub.com/dotnet/sdk/releases/tag/v8.0.204): .NET 8.0.4 [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.203...v8.0.204) [Release](https://togithub.com/dotnet/core/releases/tag/v8.0.4)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 8d00746b..e732a3b4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.203" + "version": "8.0.204" } } From ff9df593400f92c016eee1a45bd7097da008d4dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 May 2024 13:25:21 +1000 Subject: [PATCH 176/316] chore(deps): update codecov/codecov-action action to v4.3.1 (#267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [codecov/codecov-action](https://togithub.com/codecov/codecov-action) | action | patch | `v4.3.0` -> `v4.3.1` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v4.3.1`](https://togithub.com/codecov/codecov-action/compare/v4.3.0...v4.3.1) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.3.0...v4.3.1)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](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 b681d66d..010ed660 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -37,7 +37,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v4.3.0 + - uses: codecov/codecov-action@v4.3.1 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From 67a1a0aea95ee943976990b1d1782e4061300b50 Mon Sep 17 00:00:00 2001 From: Justin Abrahms Date: Thu, 16 May 2024 06:58:34 -0700 Subject: [PATCH 177/316] chore: Support for determining spec support for the repo (#270) Signed-off-by: Justin Abrahms --- .gitignore | 2 ++ .specrc | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 .specrc diff --git a/.gitignore b/.gitignore index 575a49ad..a44a107a 100644 --- a/.gitignore +++ b/.gitignore @@ -352,3 +352,5 @@ ASALocalRun/ # integration tests test/OpenFeature.E2ETests/Features/evaluation.feature test/OpenFeature.E2ETests/Features/evaluation.feature.cs +cs-report.json +specification.json diff --git a/.specrc b/.specrc new file mode 100644 index 00000000..03435c01 --- /dev/null +++ b/.specrc @@ -0,0 +1,5 @@ +[spec] +file_extension=cs +multiline_regex=\[Specification\((?P.*?)\)\] +number_subregex=([\d.]+) +text_subregex=,\s+"(.*)" From 581ff81c7b1840c34840229bf20444c528c64cc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 06:44:24 +0100 Subject: [PATCH 178/316] chore(deps): update dependency microsoft.net.test.sdk to v17.10.0 (#273) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 14e21a27..757f24c9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From acd0385641e114a16d0ee56e3a143baa7d3c0535 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 08:35:42 -0700 Subject: [PATCH 179/316] chore(deps): update dependency dotnet-sdk to v8.0.301 (#271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dotnet-sdk](https://togithub.com/dotnet/sdk) | dotnet-sdk | patch | `8.0.204` -> `8.0.301` | --- ### Release Notes
dotnet/sdk (dotnet-sdk) ### [`v8.0.301`](https://togithub.com/dotnet/sdk/compare/v8.0.300...v8.0.301) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.300...v8.0.301) ### [`v8.0.300`](https://togithub.com/dotnet/sdk/releases/tag/v8.0.300): .NET 8.0.5 [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.206...v8.0.300) [Release](https://togithub.com/dotnet/core/releases/tag/v8.0.5) ### [`v8.0.206`](https://togithub.com/dotnet/sdk/compare/v8.0.205...v8.0.206) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.205...v8.0.206) ### [`v8.0.205`](https://togithub.com/dotnet/sdk/compare/v8.0.204...v8.0.205) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.204...v8.0.205)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index e732a3b4..635d63fc 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.204" + "version": "8.0.301" } } From 33154d2ed6b0b27f4a86a5fbad440a784a89c881 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 17 Jun 2024 13:33:54 -0400 Subject: [PATCH 180/316] feat!: add CancellationTokens, ValueTasks hooks (#268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is a combination of https://github.com/open-feature/dotnet-sdk/pull/184 and https://github.com/open-feature/dotnet-sdk/pull/185. Changes include: - adding cancellation tokens - in all cases where async operations include side-effects (`setProviderAsync`, `InitializeAsync`, I've specified in the in-line doc that the cancellation token's purpose is to cancel such side-effects - so setting a provider and canceling that operation still results in that provider's being set, but async side-effect should be cancelled. I'm interested in feedback here, I think we need to consider the semantics around this... I suppose the alternative would be to always ensure any state changes only occur after async side-effects, if they weren't cancelled beforehand. - adding "Async" suffix to all async methods - remove deprecated sync `SetProvider` methods - Using `ValueTask` for hook methods - I've decided against converting all `Tasks` to `ValueTasks`, from the [official .NET docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-8.0): > the default choice for any asynchronous method that does not return a result should be to return a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=net-8.0). Only if performance analysis proves it worthwhile should a ValueTask be used instead of a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=net-8.0). - I think for hooks, `ValueTask` especially makes sense since often hooks are synchronous, in fact async hooks are probably the less likely variant. - I've kept the resolver methods as `Task`, but there could be an argument for making them `ValueTask`, since some providers resolve asynchronously. - I'm still a bit dubious on the entire idea of `ValueTask`, so I'm really interested in feedback here - associated test updates UPDATE: After chewing on this for a night, I'm starting to feel: - We should simply remove cancellation tokens from Init/Shutdown. We can always add them later, which would be non-breaking. I think the value is low and the complexity is potentially high. - ValueTask is only a good idea for hooks, because: - Hooks will very often be synchronous under the hood - We (SDK authors) await the hooks, not consumer code, so we can be careful of the potential pitfalls of ValueTask. I think everywhere else we should stick to Task. --------- Signed-off-by: Austin Drenski Signed-off-by: Todd Baert Co-authored-by: Austin Drenski Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- README.md | 640 +++++++++--------- src/OpenFeature/Api.cs | 33 +- src/OpenFeature/EventExecutor.cs | 7 +- src/OpenFeature/FeatureProvider.cs | 34 +- src/OpenFeature/Hook.cs | 27 +- src/OpenFeature/IFeatureClient.cs | 31 +- src/OpenFeature/NoOpProvider.cs | 11 +- src/OpenFeature/OpenFeatureClient.cs | 118 ++-- src/OpenFeature/ProviderRepository.cs | 34 +- .../Providers/Memory/InMemoryProvider.cs | 28 +- .../OpenFeatureClientBenchmarks.cs | 30 +- .../Steps/EvaluationStepDefinitions.cs | 28 +- .../OpenFeature.Tests/FeatureProviderTests.cs | 38 +- .../OpenFeatureClientTests.cs | 146 ++-- .../OpenFeatureEventTests.cs | 22 +- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 359 ++++++---- test/OpenFeature.Tests/OpenFeatureTests.cs | 50 +- .../ProviderRepositoryTests.cs | 200 +++--- .../Providers/Memory/InMemoryProviderTests.cs | 51 +- test/OpenFeature.Tests/TestImplementations.cs | 49 +- test/OpenFeature.Tests/TestUtilsTest.cs | 5 +- 21 files changed, 1001 insertions(+), 940 deletions(-) diff --git a/README.md b/README.md index 6cb3c35c..6844915f 100644 --- a/README.md +++ b/README.md @@ -1,320 +1,320 @@ - - - -![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) - -## .NET SDK - - - -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) -[ - ![Release](https://img.shields.io/static/v1?label=release&message=v1.5.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.5.0) - -[![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -[![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) -[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) - - -[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. - - - -## πŸš€ Quick start - -### Requirements - -- .NET 6+ -- .NET Core 6+ -- .NET Framework 4.6.2+ - -Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 - -### Install - -Use the following to initialize your project: - -```sh -dotnet new console -``` - -and install OpenFeature: - -```sh -dotnet add package OpenFeature -``` - -### Usage - -```csharp -public async Task Example() -{ - // Register your feature flag provider - await Api.Instance.SetProvider(new InMemoryProvider()); - - // Create a new client - FeatureClient client = Api.Instance.GetClient(); - - // Evaluate your feature flag - bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); - - if ( v2Enabled ) - { - //Do some work - } -} -``` - -## 🌟 Features - -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| βœ… | [Logging](#logging) | Integrate with popular logging packages. | -| βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | - -> Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ - -### Providers - -[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. -Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). - -If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. - -Once you've added a provider as a dependency, it can be registered with OpenFeature like this: - -```csharp -await Api.Instance.SetProvider(new MyProvider()); -``` - -In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more detail below. - -### Targeting - -Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. -In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). -If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). - -```csharp -// set a value to the global context -EvaluationContextBuilder builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext apiCtx = builder.Build(); -Api.Instance.SetContext(apiCtx); - -// set a value to the client context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext clientCtx = builder.Build(); -var client = Api.Instance.GetClient(); -client.SetContext(clientCtx); - -// set a value to the invocation context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext reqCtx = builder.Build(); - -bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); - -``` - -### Hooks - -[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. -If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. - -Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. - -```csharp -// add a hook globally, to run on all evaluations -Api.Instance.AddHooks(new ExampleGlobalHook()); - -// add a hook on this client, to run on all evaluations made by this client -var client = Api.Instance.GetClient(); -client.AddHooks(new ExampleClientHook()); - -// add a hook for this evaluation only -var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); -``` - -### Logging - -The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. - -### Named clients - -Clients can be given a name. -A name is a logical identifier that can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. - -```csharp -// registering the default provider -await Api.Instance.SetProvider(new LocalProvider()); - -// registering a named provider -await Api.Instance.SetProvider("clientForCache", new CachedProvider()); - -// a client backed by default provider -FeatureClient clientDefault = Api.Instance.GetClient(); - -// a client backed by CachedProvider -FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); - -``` - -### Eventing - -Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, -provider readiness, or error conditions. -Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. -Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. - -Please refer to the documentation of the provider you're using to see what events are supported. - -Example usage of an Event handler: - -```csharp -public static void EventHandler(ProviderEventPayload eventDetails) -{ - Console.WriteLine(eventDetails.Type); -} -``` - -```csharp -EventHandlerDelegate callback = EventHandler; -// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event -Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -It is also possible to register an event handler for a specific client, as in the following example: - -```csharp -EventHandlerDelegate callback = EventHandler; - -var myClient = Api.Instance.GetClient("my-client"); - -var provider = new ExampleProvider(); -await Api.Instance.SetProvider(myClient.GetMetadata().Name, provider); - -myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -### Shutdown - -The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. - -```csharp -// Shut down all providers -await Api.Instance.Shutdown(); -``` - -## Extending - -### Develop a provider - -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. - -```csharp -public class MyProvider : FeatureProvider -{ - public override Metadata GetMetadata() - { - return new Metadata("My Provider"); - } - - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) - { - // resolve a boolean flag value - } - - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) - { - // resolve a double flag value - } - - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) - { - // resolve an int flag value - } - - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) - { - // resolve a string flag value - } - - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) - { - // resolve an object flag value - } -} -``` - -### Develop a hook - -To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -Implement your own hook by conforming to the `Hook interface`. -To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. - -```csharp -public class MyHook : Hook -{ - public Task Before(HookContext context, - IReadOnlyDictionary hints = null) - { - // code to run before flag evaluation - } - - public virtual Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) - { - // code to run after successful flag evaluation - } - - public virtual Task Error(HookContext context, Exception error, - IReadOnlyDictionary hints = null) - { - // code to run if there's an error during before hooks or during flag evaluation - } - - public virtual Task Finally(HookContext context, IReadOnlyDictionary hints = null) - { - // code to run after all other stages, regardless of success/failure - } -} -``` - -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! - - -## ⭐️ Support the project - -- Give this repo a ⭐️! -- Follow us on social media: - - Twitter: [@openfeature](https://twitter.com/openfeature) - - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) -- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more information, check out our [community page](https://openfeature.dev/community/) - -## 🀝 Contributing - -Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. - -### Thanks to everyone who has already contributed - -[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) - -Made with [contrib.rocks](https://contrib.rocks). - + + + +![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) + +## .NET SDK + + + +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) +[ + ![Release](https://img.shields.io/static/v1?label=release&message=v1.5.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.5.0) + +[![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) +[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) + + +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. + + + +## πŸš€ Quick start + +### Requirements + +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ + +Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 + +### Install + +Use the following to initialize your project: + +```sh +dotnet new console +``` + +and install OpenFeature: + +```sh +dotnet add package OpenFeature +``` + +### Usage + +```csharp +public async Task Example() +{ + // Register your feature flag provider + await Api.Instance.SetProviderAsync(new InMemoryProvider()); + + // Create a new client + FeatureClient client = Api.Instance.GetClient(); + + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false); + + if ( v2Enabled ) + { + //Do some work + } +} +``` + +## 🌟 Features + +| Status | Features | Description | +| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Logging](#logging) | Integrate with popular logging packages. | +| βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | +| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | + +> Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ + +### Providers + +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). + +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. + +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +```csharp +await Api.Instance.SetProviderAsync(new MyProvider()); +``` + +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [named clients](#named-clients), which is covered in more detail below. + +### Targeting + +Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). + +```csharp +// set a value to the global context +EvaluationContextBuilder builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext apiCtx = builder.Build(); +Api.Instance.SetContext(apiCtx); + +// set a value to the client context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext clientCtx = builder.Build(); +var client = Api.Instance.GetClient(); +client.SetContext(clientCtx); + +// set a value to the invocation context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext reqCtx = builder.Build(); + +bool flagValue = await client.GetBooleanValuAsync("some-flag", false, reqCtx); + +``` + +### Hooks + +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. + +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. + +```csharp +// add a hook globally, to run on all evaluations +Api.Instance.AddHooks(new ExampleGlobalHook()); + +// add a hook on this client, to run on all evaluations made by this client +var client = Api.Instance.GetClient(); +client.AddHooks(new ExampleClientHook()); + +// add a hook for this evaluation only +var value = await client.GetBooleanValueAsync("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); +``` + +### Logging + +The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. + +### Named clients + +Clients can be given a name. +A name is a logical identifier that can be used to associate clients with a particular provider. +If a name has no associated provider, the global provider is used. + +```csharp +// registering the default provider +await Api.Instance.SetProviderAsync(new LocalProvider()); + +// registering a named provider +await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); + +// a client backed by default provider +FeatureClient clientDefault = Api.Instance.GetClient(); + +// a client backed by CachedProvider +FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); + +``` + +### Eventing + +Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, +provider readiness, or error conditions. +Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. +Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. + +Please refer to the documentation of the provider you're using to see what events are supported. + +Example usage of an Event handler: + +```csharp +public static void EventHandler(ProviderEventPayload eventDetails) +{ + Console.WriteLine(eventDetails.Type); +} +``` + +```csharp +EventHandlerDelegate callback = EventHandler; +// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event +Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +It is also possible to register an event handler for a specific client, as in the following example: + +```csharp +EventHandlerDelegate callback = EventHandler; + +var myClient = Api.Instance.GetClient("my-client"); + +var provider = new ExampleProvider(); +await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider); + +myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +### Shutdown + +The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. + +```csharp +// Shut down all providers +await Api.Instance.ShutdownAsync(); +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +```csharp +public class MyProvider : FeatureProvider +{ + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a boolean flag value + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a string flag value + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a double flag value + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve an object flag value + } +} +``` + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. + +```csharp +public class MyHook : Hook +{ + public ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary hints = null) + { + // code to run before flag evaluation + } + + public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null) + { + // code to run after successful flag evaluation + } + + public ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary hints = null) + { + // code to run if there's an error during before hooks or during flag evaluation + } + + public ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null) + { + // code to run after all other stages, regardless of success/failure + } +} +``` + +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! + + +## ⭐️ Support the project + +- Give this repo a ⭐️! +- Follow us on social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more information, check out our [community page](https://openfeature.dev/community/) + +## 🀝 Contributing + +Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. + +### Thanks to everyone who has already contributed + +[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) + +Made with [contrib.rocks](https://contrib.rocks). + diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index fbafa695..6f13cac2 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -37,40 +37,17 @@ static Api() { } private Api() { } /// - /// Sets the default feature provider to given clientName without awaiting its initialization. - /// - /// The provider cannot be set to null. Attempting to set the provider to null has no effect. - /// Implementation of - [Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")] - public void SetProvider(FeatureProvider featureProvider) - { - this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - _ = this._repository.SetProvider(featureProvider, this.GetContext()); - } - - /// - /// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete, + /// Sets the 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. /// Implementation of - public async Task SetProviderAsync(FeatureProvider? featureProvider) + public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext()).ConfigureAwait(false); } - /// - /// Sets the feature provider to given clientName without awaiting its initialization. - /// - /// Name of client - /// Implementation of - [Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")] - public void SetProvider(string clientName, FeatureProvider featureProvider) - { - this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - _ = this._repository.SetProvider(clientName, featureProvider, this.GetContext()); - } /// /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and @@ -85,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro throw new ArgumentNullException(nameof(clientName)); } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); } /// @@ -248,7 +225,7 @@ public EvaluationContext GetContext() /// Once shut down is complete, API is reset and ready to use again. /// /// - public async Task Shutdown() + public async Task ShutdownAsync() { await using (this._eventExecutor.ConfigureAwait(false)) await using (this._repository.ConfigureAwait(false)) diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 816bf13e..886a47b6 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -10,6 +10,8 @@ namespace OpenFeature { + internal delegate Task ShutdownDelegate(CancellationToken cancellationToken); + internal sealed partial class EventExecutor : IAsyncDisposable { private readonly object _lockObj = new object(); @@ -30,7 +32,7 @@ public EventExecutor() eventProcessing.Start(); } - public ValueTask DisposeAsync() => new(this.Shutdown()); + public ValueTask DisposeAsync() => new(this.ShutdownAsync()); internal void SetLogger(ILogger logger) => this._logger = logger; @@ -317,10 +319,9 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } } - public async Task Shutdown() + public async Task ShutdownAsync() { this.EventChannel.Writer.Complete(); - await this.EventChannel.Reader.Completion.ConfigureAwait(false); } diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index bcc66558..62976f53 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using OpenFeature.Constant; @@ -43,9 +44,10 @@ public abstract class FeatureProvider /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a string feature flag @@ -53,9 +55,10 @@ public abstract Task> ResolveBooleanValue(string flagKey /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a integer feature flag @@ -63,9 +66,10 @@ public abstract Task> ResolveStringValue(string flagKe /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a double feature flag @@ -73,9 +77,10 @@ public abstract Task> ResolveIntegerValue(string flagKey, /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Resolves a structured feature flag @@ -83,9 +88,10 @@ public abstract Task> ResolveDoubleValue(string flagKe /// Feature flag key /// Default value /// + /// The . /// - public abstract Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext? context = null); + public abstract Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default); /// /// Get the status of the provider. @@ -95,7 +101,7 @@ public abstract Task> ResolveStructureValue(string flag /// If a provider does not override this method, then its status will be assumed to be /// . If a provider implements this method, and supports initialization, /// then it should start in the status . If the status is - /// , then the Api will call the when the + /// , then the Api will call the when the /// provider is set. /// public virtual ProviderStatus GetStatus() => ProviderStatus.Ready; @@ -107,6 +113,7 @@ public abstract Task> ResolveStructureValue(string flag /// /// /// + /// The to cancel any async side effects. /// A task that completes when the initialization process is complete. /// /// @@ -118,7 +125,7 @@ public abstract Task> ResolveStructureValue(string flag /// the method after initialization is complete. /// /// - public virtual Task Initialize(EvaluationContext context) + public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { // Intentionally left blank. return Task.CompletedTask; @@ -129,7 +136,8 @@ public virtual Task Initialize(EvaluationContext context) /// Providers can overwrite this method, if they have special shutdown actions needed. /// /// A task that completes when the shutdown process is complete. - public virtual Task Shutdown() + /// The to cancel any async side effects. + public virtual Task ShutdownAsync(CancellationToken cancellationToken = default) { // Intentionally left blank. return Task.CompletedTask; diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs index 50162729..aea5dc15 100644 --- a/src/OpenFeature/Hook.cs +++ b/src/OpenFeature/Hook.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Model; @@ -26,12 +27,13 @@ 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 Task Before(HookContext context, - IReadOnlyDictionary? hints = null) + public virtual ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.FromResult(EvaluationContext.Empty); + return new ValueTask(EvaluationContext.Empty); } /// @@ -40,11 +42,12 @@ public virtual Task Before(HookContext context, /// Provides context of innovation /// Flag evaluation information /// Caller provided data + /// The . /// Flag value type (bool|number|string|object) - public virtual Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null) + public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } /// @@ -53,11 +56,12 @@ public virtual Task After(HookContext context, FlagEvaluationDetails de /// Provides context of innovation /// Exception representing what went wrong /// Caller provided data + /// The . /// Flag value type (bool|number|string|object) - public virtual Task Error(HookContext context, Exception error, - IReadOnlyDictionary? hints = null) + public virtual ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } /// @@ -65,10 +69,11 @@ public virtual Task Error(HookContext context, Exception error, /// /// Provides context of innovation /// Caller provided data + /// The . /// Flag value type (bool|number|string|object) - public virtual Task Finally(HookContext context, IReadOnlyDictionary? hints = null) + public virtual ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } } } diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index b262f8f1..4a09c5e8 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Model; @@ -59,8 +60,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a boolean feature flag @@ -69,8 +71,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetBooleanDetails(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a string feature flag @@ -79,8 +82,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a string feature flag @@ -89,8 +93,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetStringDetails(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetStringDetailsAsync(string flagKey, string defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a integer feature flag @@ -99,8 +104,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a integer feature flag @@ -109,8 +115,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetIntegerDetails(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a double feature flag @@ -119,8 +126,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a double feature flag @@ -129,8 +137,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetDoubleDetails(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a structure object feature flag @@ -139,8 +148,9 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag value. - Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); /// /// Resolves a structure object feature flag @@ -149,7 +159,8 @@ public interface IFeatureClient : IEventBus /// Default value /// Evaluation Context /// Flag Evaluation Options + /// The . /// Resolved flag details - Task> GetObjectDetails(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null); + Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default); } } diff --git a/src/OpenFeature/NoOpProvider.cs b/src/OpenFeature/NoOpProvider.cs index 693e504e..5d7b9caa 100644 --- a/src/OpenFeature/NoOpProvider.cs +++ b/src/OpenFeature/NoOpProvider.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; @@ -13,27 +14,27 @@ public override Metadata GetMetadata() return this._metadata; } - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null) + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext? context = null) + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null) + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext? context = null) + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(NoOpResponse(flagKey, defaultValue)); } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 6145094e..674b78a7 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -36,9 +37,9 @@ public sealed partial class FeatureClient : IFeatureClient /// /// The type of the resolution method /// A tuple containing a resolution method and the provider it came from. - private (Func>>, FeatureProvider) + private (Func>>, FeatureProvider) ExtractProvider( - Func>>> method) + Func>>> method) { // Alias the provider reference so getting the method and returning the provider are // guaranteed to be the same object. @@ -136,70 +137,71 @@ public void AddHooks(IEnumerable hooks) public void ClearHooks() => this._hooks.Clear(); /// - public async Task GetBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetBooleanDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetBooleanDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetBooleanDetails(string flagKey, bool defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveBooleanValue), + public async Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveBooleanValueAsync), FlagValueType.Boolean, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetStringValue(string flagKey, string defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetStringDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetStringDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetStringDetails(string flagKey, string defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStringValue), + public async Task> GetStringDetailsAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStringValueAsync), FlagValueType.String, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetIntegerDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetIntegerDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetIntegerDetails(string flagKey, int defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveIntegerValue), + public async Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveIntegerValueAsync), FlagValueType.Number, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetDoubleValue(string flagKey, double defaultValue, + public async Task GetDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetDoubleDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetDoubleDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetDoubleDetails(string flagKey, double defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveDoubleValue), + public async Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveDoubleValueAsync), FlagValueType.Number, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); /// - public async Task GetObjectValue(string flagKey, Value defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null) => - (await this.GetObjectDetails(flagKey, defaultValue, context, config).ConfigureAwait(false)).Value; + public async Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetObjectDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; /// - public async Task> GetObjectDetails(string flagKey, Value defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null) => - await this.EvaluateFlag(this.ExtractProvider(provider => provider.ResolveStructureValue), + public async Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStructureValueAsync), FlagValueType.Object, flagKey, - defaultValue, context, config).ConfigureAwait(false); + defaultValue, context, config, cancellationToken).ConfigureAwait(false); - private async Task> EvaluateFlag( - (Func>>, FeatureProvider) providerInfo, + private async Task> EvaluateFlagAsync( + (Func>>, FeatureProvider) providerInfo, FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? options = null) + FlagEvaluationOptions? options = null, + CancellationToken cancellationToken = default) { var resolveValueDelegate = providerInfo.Item1; var provider = providerInfo.Item2; @@ -242,45 +244,45 @@ private async Task> EvaluateFlag( FlagEvaluationDetails evaluation; try { - var contextFromHooks = await this.TriggerBeforeHooks(allHooks, hookContext, options).ConfigureAwait(false); + var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false); evaluation = - (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext).ConfigureAwait(false)) + (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false)) .ToFlagEvaluationDetails(); - await this.TriggerAfterHooks(allHooksReversed, hookContext, evaluation, options).ConfigureAwait(false); + await this.TriggerAfterHooksAsync(allHooksReversed, hookContext, evaluation, options, cancellationToken).ConfigureAwait(false); } catch (FeatureProviderException ex) { this.FlagEvaluationErrorWithDescription(flagKey, ex.ErrorType.GetDescription(), ex); evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error, string.Empty, ex.Message); - await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); + await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, ex, options, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { this.FlagEvaluationError(flagKey, ex); var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General; evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty, ex.Message); - await this.TriggerErrorHooks(allHooksReversed, hookContext, ex, options).ConfigureAwait(false); + await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, ex, options, cancellationToken).ConfigureAwait(false); } finally { - await this.TriggerFinallyHooks(allHooksReversed, hookContext, options).ConfigureAwait(false); + await this.TriggerFinallyHooksAsync(allHooksReversed, hookContext, options, cancellationToken).ConfigureAwait(false); } return evaluation; } - private async Task> TriggerBeforeHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions? options) + 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.Before(context, options?.HookHints).ConfigureAwait(false); + var resp = await hook.BeforeAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false); if (resp != null) { evalContextBuilder.Merge(resp); @@ -295,23 +297,23 @@ private async Task> TriggerBeforeHooks(IReadOnlyList hoo return context.WithNewEvaluationContext(evalContextBuilder.Build()); } - private async Task TriggerAfterHooks(IReadOnlyList hooks, HookContext context, - FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options) + private async Task TriggerAfterHooksAsync(IReadOnlyList hooks, HookContext context, + FlagEvaluationDetails evaluationDetails, FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) { - await hook.After(context, evaluationDetails, options?.HookHints).ConfigureAwait(false); + await hook.AfterAsync(context, evaluationDetails, options?.HookHints, cancellationToken).ConfigureAwait(false); } } - private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext context, Exception exception, - FlagEvaluationOptions? options) + private async Task TriggerErrorHooksAsync(IReadOnlyList hooks, HookContext context, Exception exception, + FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) { try { - await hook.Error(context, exception, options?.HookHints).ConfigureAwait(false); + await hook.ErrorAsync(context, exception, options?.HookHints, cancellationToken).ConfigureAwait(false); } catch (Exception e) { @@ -320,14 +322,14 @@ private async Task TriggerErrorHooks(IReadOnlyList hooks, HookContext(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions? options) + private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, HookContext context, + FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) { try { - await hook.Finally(context, options?.HookHints).ConfigureAwait(false); + await hook.FinallyAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false); } catch (Exception e) { diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 5b331d43..7934da1c 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -35,7 +35,7 @@ public async ValueTask DisposeAsync() { using (this._providersLock) { - await this.Shutdown().ConfigureAwait(false); + await this.ShutdownAsync().ConfigureAwait(false); } } @@ -62,7 +62,7 @@ public async ValueTask DisposeAsync() /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - public async Task SetProvider( + public async Task SetProviderAsync( FeatureProvider? featureProvider, EvaluationContext context, Action? afterSet = null, @@ -92,7 +92,7 @@ public async Task SetProvider( // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. #pragma warning disable CS4014 - this.ShutdownIfUnused(oldProvider, afterShutdown, afterError); + this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); #pragma warning restore CS4014 } finally @@ -100,11 +100,11 @@ public async Task SetProvider( this._providersLock.ExitWriteLock(); } - await InitProvider(this._defaultProvider, context, afterInitialization, afterError) + await InitProviderAsync(this._defaultProvider, context, afterInitialization, afterError) .ConfigureAwait(false); } - private static async Task InitProvider( + private static async Task InitProviderAsync( FeatureProvider? newProvider, EvaluationContext context, Action? afterInitialization, @@ -118,7 +118,7 @@ private static async Task InitProvider( { try { - await newProvider.Initialize(context).ConfigureAwait(false); + await newProvider.InitializeAsync(context).ConfigureAwait(false); afterInitialization?.Invoke(newProvider); } catch (Exception ex) @@ -152,13 +152,15 @@ private static async Task InitProvider( /// initialization /// /// called after a provider is shutdown, can be used to remove event handlers - public async Task SetProvider(string? clientName, + /// The to cancel any async side effects. + public async Task SetProviderAsync(string clientName, FeatureProvider? featureProvider, EvaluationContext context, Action? afterSet = null, Action? afterInitialization = null, Action? afterError = null, - Action? afterShutdown = null) + Action? afterShutdown = null, + CancellationToken cancellationToken = default) { // Cannot set a provider for a null clientName. if (clientName == null) @@ -187,7 +189,7 @@ public async Task SetProvider(string? clientName, // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. #pragma warning disable CS4014 - this.ShutdownIfUnused(oldProvider, afterShutdown, afterError); + this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); #pragma warning restore CS4014 } finally @@ -195,13 +197,13 @@ public async Task SetProvider(string? clientName, this._providersLock.ExitWriteLock(); } - await InitProvider(featureProvider, context, afterInitialization, afterError).ConfigureAwait(false); + await InitProviderAsync(featureProvider, context, afterInitialization, afterError).ConfigureAwait(false); } /// /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. /// - private async Task ShutdownIfUnused( + private async Task ShutdownIfUnusedAsync( FeatureProvider? targetProvider, Action? afterShutdown, Action? afterError) @@ -216,7 +218,7 @@ private async Task ShutdownIfUnused( return; } - await SafeShutdownProvider(targetProvider, afterShutdown, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider, afterShutdown, afterError).ConfigureAwait(false); } /// @@ -228,7 +230,7 @@ private async Task ShutdownIfUnused( /// it would not be meaningful to emit an error. /// /// - private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, + private static async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider, Action? afterShutdown, Action? afterError) { @@ -239,7 +241,7 @@ private static async Task SafeShutdownProvider(FeatureProvider? targetProvider, try { - await targetProvider.Shutdown().ConfigureAwait(false); + await targetProvider.ShutdownAsync().ConfigureAwait(false); afterShutdown?.Invoke(targetProvider); } catch (Exception ex) @@ -281,7 +283,7 @@ public FeatureProvider GetProvider(string? clientName) : this.GetProvider(); } - public async Task Shutdown(Action? afterError = null) + public async Task ShutdownAsync(Action? afterError = null, CancellationToken cancellationToken = default) { var providers = new HashSet(); this._providersLock.EnterWriteLock(); @@ -305,7 +307,7 @@ public async Task Shutdown(Action? afterError = null foreach (var targetProvider in providers) { // We don't need to take any actions after shutdown. - await SafeShutdownProvider(targetProvider, null, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider, null, afterError).ConfigureAwait(false); } } } diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 766e4f3c..e56acdb5 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Error; @@ -45,7 +46,7 @@ public InMemoryProvider(IDictionary? flags = null) /// Updating provider flags configuration, replacing all flags. /// /// the flags to use instead of the previous flags. - public async ValueTask UpdateFlags(IDictionary? flags = null) + public async Task UpdateFlags(IDictionary? flags = null) { var changed = this._flags.Keys.ToList(); if (flags == null) @@ -68,46 +69,31 @@ public async ValueTask UpdateFlags(IDictionary? flags = null) } /// - public override Task> ResolveBooleanValue( - string flagKey, - bool defaultValue, - EvaluationContext? context = null) + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveStringValue( - string flagKey, - string defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveIntegerValue( - string flagKey, - int defaultValue, - EvaluationContext? context = null) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveDoubleValue( - string flagKey, - double defaultValue, - EvaluationContext? context = null) + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } /// - public override Task> ResolveStructureValue( - string flagKey, - Value defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 03f6082a..7f2e5b30 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -45,62 +45,62 @@ public OpenFeatureClientBenchmarks() [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValue(_flagName, _defaultBoolValue); + await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValue(_flagName, _defaultBoolValue, EvaluationContext.Empty); + await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetBooleanValue(_flagName, _defaultBoolValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValue(_flagName, _defaultStringValue); + await _client.GetStringValueAsync(_flagName, _defaultStringValue); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValue(_flagName, _defaultStringValue, EvaluationContext.Empty); + await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetStringValue(_flagName, _defaultStringValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValue(_flagName, _defaultIntegerValue); + await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValue(_flagName, _defaultIntegerValue, EvaluationContext.Empty); + await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetIntegerValue(_flagName, _defaultIntegerValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValue(_flagName, _defaultDoubleValue); + await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValue(_flagName, _defaultDoubleValue, EvaluationContext.Empty); + await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetDoubleValue(_flagName, _defaultDoubleValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty, _emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValue(_flagName, _defaultStructureValue); + await _client.GetObjectValueAsync(_flagName, _defaultStructureValue); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValue(_flagName, _defaultStructureValue, EvaluationContext.Empty); + await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetObjectValue(_flagName, _defaultStructureValue, EvaluationContext.Empty, _emptyFlagOptions); + await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty, _emptyFlagOptions); } } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index b7b1f9b5..a50f3945 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -49,7 +49,7 @@ public void GivenAProviderIsRegistered() [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagValue = client?.GetBooleanValue(flagKey, defaultValue); + this.booleanFlagValue = client?.GetBooleanValueAsync(flagKey, defaultValue); } [Then(@"the resolved boolean value should be ""(.*)""")] @@ -61,7 +61,7 @@ public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - this.stringFlagValue = client?.GetStringValue(flagKey, defaultValue); + this.stringFlagValue = client?.GetStringValueAsync(flagKey, defaultValue); } [Then(@"the resolved string value should be ""(.*)""")] @@ -73,7 +73,7 @@ public void Thentheresolvedstringvalueshouldbe(string expected) [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) { - this.intFlagValue = client?.GetIntegerValue(flagKey, defaultValue); + this.intFlagValue = client?.GetIntegerValueAsync(flagKey, defaultValue); } [Then(@"the resolved integer value should be (.*)")] @@ -85,7 +85,7 @@ public void Thentheresolvedintegervalueshouldbe(int expected) [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagValue = client?.GetDoubleValue(flagKey, defaultValue); + this.doubleFlagValue = client?.GetDoubleValueAsync(flagKey, defaultValue); } [Then(@"the resolved float value should be (.*)")] @@ -97,7 +97,7 @@ public void Thentheresolvedfloatvalueshouldbe(double expected) [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) { - this.objectFlagValue = client?.GetObjectValue(flagKey, new Value()); + this.objectFlagValue = client?.GetObjectValueAsync(flagKey, new Value()); } [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] @@ -112,7 +112,7 @@ public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespe [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagDetails = client?.GetBooleanDetails(flagKey, defaultValue); + this.booleanFlagDetails = client?.GetBooleanDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -127,7 +127,7 @@ public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthere [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) { - this.stringFlagDetails = client?.GetStringDetails(flagKey, defaultValue); + this.stringFlagDetails = client?.GetStringDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -142,7 +142,7 @@ public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandtherea [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) { - this.intFlagDetails = client?.GetIntegerDetails(flagKey, defaultValue); + this.intFlagDetails = client?.GetIntegerDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -157,7 +157,7 @@ public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthere [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagDetails = client?.GetDoubleDetails(flagKey, defaultValue); + this.doubleFlagDetails = client?.GetDoubleDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] @@ -172,7 +172,7 @@ public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereas [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) { - this.objectFlagDetails = client?.GetObjectDetails(flagKey, new Value()); + this.objectFlagDetails = client?.GetObjectDetailsAsync(flagKey, new Value()); } [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] @@ -206,7 +206,7 @@ public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string { contextAwareFlagKey = flagKey; contextAwareDefaultValue = defaultValue; - contextAwareValue = client?.GetStringValue(flagKey, contextAwareDefaultValue, context)?.Result; + contextAwareValue = client?.GetStringValueAsync(flagKey, contextAwareDefaultValue, context)?.Result; } [Then(@"the resolved string response should be ""(.*)""")] @@ -218,7 +218,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string? emptyContextValue = client?.GetStringValue(contextAwareFlagKey!, contextAwareDefaultValue!, new EvaluationContextBuilder().Build()).Result; + string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey!, contextAwareDefaultValue!, EvaluationContext.Empty).Result; Assert.Equal(expected, emptyContextValue); } @@ -227,7 +227,7 @@ public void Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultva { this.notFoundFlagKey = flagKey; this.notFoundDefaultValue = defaultValue; - this.notFoundDetails = client?.GetStringDetails(this.notFoundFlagKey, this.notFoundDefaultValue).Result; + this.notFoundDetails = client?.GetStringDetailsAsync(this.notFoundFlagKey, this.notFoundDefaultValue).Result; } [Then(@"the default string value should be returned")] @@ -248,7 +248,7 @@ public void Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultva { this.typeErrorFlagKey = flagKey; this.typeErrorDefaultValue = defaultValue; - this.typeErrorDetails = client?.GetIntegerDetails(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; + this.typeErrorDetails = client?.GetIntegerDetailsAsync(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; } [Then(@"the default integer value should be returned")] diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index 8d679f94..53a67443 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -44,27 +44,27 @@ public async Task Provider_Must_Resolve_Flag_Values() var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveBooleanValue(flagName, defaultBoolValue)).Should() + (await provider.ResolveBooleanValueAsync(flagName, defaultBoolValue)).Should() .BeEquivalentTo(boolResolutionDetails); var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveIntegerValue(flagName, defaultIntegerValue)).Should() + (await provider.ResolveIntegerValueAsync(flagName, defaultIntegerValue)).Should() .BeEquivalentTo(integerResolutionDetails); var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveDoubleValue(flagName, defaultDoubleValue)).Should() + (await provider.ResolveDoubleValueAsync(flagName, defaultDoubleValue)).Should() .BeEquivalentTo(doubleResolutionDetails); var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStringValue(flagName, defaultStringValue)).Should() + (await provider.ResolveStringValueAsync(flagName, defaultStringValue)).Should() .BeEquivalentTo(stringResolutionDetails); var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStructureValue(flagName, defaultStructureValue)).Should() + (await provider.ResolveStructureValueAsync(flagName, defaultStructureValue)).Should() .BeEquivalentTo(structureResolutionDetails); } @@ -84,59 +84,59 @@ public async Task Provider_Must_ErrorType() var providerMock = Substitute.For(); const string testMessage = "An error message"; - providerMock.ResolveBooleanValue(flagName, defaultBoolValue, Arg.Any()) + providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveIntegerValue(flagName, defaultIntegerValue, Arg.Any()) + providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveDoubleValue(flagName, defaultDoubleValue, Arg.Any()) + providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.InvalidContext, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveStringValue(flagName, defaultStringValue, Arg.Any()) + providerMock.ResolveStringValueAsync(flagName, defaultStringValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveStructureValue(flagName, defaultStructureValue, Arg.Any()) + providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue, Arg.Any()) .Returns(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveStructureValue(flagName2, defaultStructureValue, Arg.Any()) + providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue, Arg.Any()) .Returns(new ResolutionDetails(flagName2, defaultStructureValue, ErrorType.ProviderNotReady, NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - providerMock.ResolveBooleanValue(flagName2, defaultBoolValue, Arg.Any()) + providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue, Arg.Any()) .Returns(new ResolutionDetails(flagName2, defaultBoolValue, ErrorType.TargetingKeyMissing, NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - var boolRes = await providerMock.ResolveBooleanValue(flagName, defaultBoolValue); + var boolRes = await providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue); boolRes.ErrorType.Should().Be(ErrorType.General); boolRes.ErrorMessage.Should().Be(testMessage); - var intRes = await providerMock.ResolveIntegerValue(flagName, defaultIntegerValue); + var intRes = await providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue); intRes.ErrorType.Should().Be(ErrorType.ParseError); intRes.ErrorMessage.Should().Be(testMessage); - var doubleRes = await providerMock.ResolveDoubleValue(flagName, defaultDoubleValue); + var doubleRes = await providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue); doubleRes.ErrorType.Should().Be(ErrorType.InvalidContext); doubleRes.ErrorMessage.Should().Be(testMessage); - var stringRes = await providerMock.ResolveStringValue(flagName, defaultStringValue); + var stringRes = await providerMock.ResolveStringValueAsync(flagName, defaultStringValue); stringRes.ErrorType.Should().Be(ErrorType.TypeMismatch); stringRes.ErrorMessage.Should().Be(testMessage); - var structRes1 = await providerMock.ResolveStructureValue(flagName, defaultStructureValue); + var structRes1 = await providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue); structRes1.ErrorType.Should().Be(ErrorType.FlagNotFound); structRes1.ErrorMessage.Should().Be(testMessage); - var structRes2 = await providerMock.ResolveStructureValue(flagName2, defaultStructureValue); + var structRes2 = await providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue); structRes2.ErrorType.Should().Be(ErrorType.ProviderNotReady); structRes2.ErrorMessage.Should().Be(testMessage); - var boolRes2 = await providerMock.ResolveBooleanValue(flagName2, defaultBoolValue); + var boolRes2 = await providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue); boolRes2.ErrorType.Should().Be(ErrorType.TargetingKeyMissing); boolRes2.ErrorMessage.Should().BeNull(); } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 7c656cec..e7c76d75 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; @@ -78,25 +79,25 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetBooleanValue(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); - (await client.GetBooleanValue(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); - (await client.GetBooleanValue(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultBoolValue); + (await client.GetBooleanValueAsync(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); + (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); + (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultBoolValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue)).Should().Be(defaultIntegerValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().Be(defaultIntegerValue); - (await client.GetIntegerValue(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValueAsync(flagName, defaultIntegerValue)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().Be(defaultIntegerValue); + (await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultIntegerValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue)).Should().Be(defaultDoubleValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().Be(defaultDoubleValue); - (await client.GetDoubleValue(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValueAsync(flagName, defaultDoubleValue)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().Be(defaultDoubleValue); + (await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultDoubleValue); - (await client.GetStringValue(flagName, defaultStringValue)).Should().Be(defaultStringValue); - (await client.GetStringValue(flagName, defaultStringValue, EvaluationContext.Empty)).Should().Be(defaultStringValue); - (await client.GetStringValue(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultStringValue); + (await client.GetStringValueAsync(flagName, defaultStringValue)).Should().Be(defaultStringValue); + (await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty)).Should().Be(defaultStringValue); + (await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultStringValue); - (await client.GetObjectValue(flagName, defaultStructureValue)).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValue(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValue(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValueAsync(flagName, defaultStructureValue)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(defaultStructureValue); + (await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(defaultStructureValue); } [Fact] @@ -125,29 +126,29 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var client = Api.Instance.GetClient(clientName, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetBooleanDetails(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetails(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetails(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); var integerFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetIntegerDetails(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetails(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetails(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); var doubleFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetDoubleDetails(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetails(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetails(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); var stringFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetStringDetails(flagName, defaultStringValue)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetails(flagName, defaultStringValue, EvaluationContext.Empty)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetails(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetailsAsync(flagName, defaultStringValue)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + (await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetObjectDetails(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetails(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetails(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetailsAsync(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + (await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); } [Fact] @@ -168,18 +169,18 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var mockedLogger = Substitute.For>(); // This will fail to case a String to TestStructure - mockedFeatureProvider.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Throws(); + mockedFeatureProvider.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(); mockedFeatureProvider.GetMetadata().Returns(new Metadata(fixture.Create())); mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(mockedFeatureProvider); var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger); - var evaluationDetails = await client.GetObjectDetails(flagName, defaultValue); + var evaluationDetails = await client.GetObjectDetailsAsync(flagName, defaultValue); evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); evaluationDetails.ErrorMessage.Should().Be(new InvalidCastException().Message); - _ = mockedFeatureProvider.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = mockedFeatureProvider.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); mockedLogger.Received(1).IsEnabled(LogLevel.Error); } @@ -194,16 +195,16 @@ public async Task Should_Resolve_BooleanValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveBooleanValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetBooleanValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetBooleanValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveBooleanValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -216,16 +217,16 @@ public async Task Should_Resolve_StringValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStringValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveStringValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetStringValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetStringValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveStringValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -238,16 +239,16 @@ public async Task Should_Resolve_IntegerValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveIntegerValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetIntegerValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetIntegerValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveIntegerValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -260,16 +261,16 @@ public async Task Should_Resolve_DoubleValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveDoubleValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetDoubleValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetDoubleValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveDoubleValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -282,16 +283,16 @@ public async Task Should_Resolve_StructureValue() var defaultValue = fixture.Create(); var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - (await client.GetObjectValue(flagName, defaultValue)).Should().Be(defaultValue); + (await client.GetObjectValueAsync(flagName, defaultValue)).Should().Be(defaultValue); - _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -305,18 +306,18 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() const string testMessage = "Couldn't parse flag data."; var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, "ERROR", null, testMessage))); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, "ERROR", null, testMessage))); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - var response = await client.GetObjectDetails(flagName, defaultValue); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); response.Reason.Should().Be(Reason.Error); response.ErrorMessage.Should().Be(testMessage); - _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] @@ -330,26 +331,55 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() const string testMessage = "Couldn't parse flag data."; var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValue(flagName, defaultValue, Arg.Any()).Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(clientName, clientVersion); - var response = await client.GetObjectDetails(flagName, defaultValue); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); response.Reason.Should().Be(Reason.Error); response.ErrorMessage.Should().Be(testMessage); - _ = featureProviderMock.Received(1).ResolveStructureValue(flagName, defaultValue, Arg.Any()); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } [Fact] - public async Task Should_Use_No_Op_When_Provider_Is_Null() + public async Task Cancellation_Token_Added_Is_Passed_To_Provider() { - await Api.Instance.SetProviderAsync(null); - var client = new FeatureClient("test", "test"); - (await client.GetIntegerValue("some-key", 12)).Should().Be(12); + var fixture = new Fixture(); + var clientName = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultString = fixture.Create(); + var cancelledReason = "cancelled"; + + var cts = new CancellationTokenSource(); + + + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => + { + var token = args.ArgAt(3); + while (!token.IsCancellationRequested) + { + await Task.Delay(10); // artificially delay until cancelled + } + return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); + }); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + + await Api.Instance.SetProviderAsync(clientName, featureProviderMock); + var client = Api.Instance.GetClient(clientName, clientVersion); + var task = client.GetStringDetailsAsync(flagName, defaultString, EvaluationContext.Empty, null, cts.Token); + cts.Cancel(); // cancel before awaiting + + var response = await task; + response.Value.Should().Be(defaultString); + response.Reason.Should().Be(cancelledReason); + _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultString, Arg.Any(), cts.Token); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 3a373c98..384928d6 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -46,7 +46,7 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() eventHandler.Received().Invoke(Arg.Is(payload => payload == myEvent.EventPayload)); // shut down the event executor - await eventExecutor.Shutdown(); + await eventExecutor.ShutdownAsync(); // the next event should not be propagated to the event handler var newEventPayload = new ProviderEventPayload @@ -78,9 +78,9 @@ public async Task API_Level_Event_Handlers_Should_Be_Registered() var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); - testProvider.SendEvent(ProviderEventTypes.ProviderError); - testProvider.SendEvent(ProviderEventTypes.ProviderStale); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderError); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderStale); await Utils.AssertUntilAsync(_ => eventHandler .Received() @@ -148,7 +148,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ var testProvider = new TestProvider(); #pragma warning disable CS0618// Type or member is obsolete - Api.Instance.SetProvider(testProvider); + await Api.Instance.SetProviderAsync(testProvider); #pragma warning restore CS0618// Type or member is obsolete Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -228,12 +228,12 @@ public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); var newTestProvider = new TestProvider(); await Api.Instance.SetProviderAsync(newTestProvider); - newTestProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await newTestProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); await Utils.AssertUntilAsync( _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)) @@ -407,7 +407,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Name client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler); - defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); // verify that the client received the event from the default provider as there is no named provider registered yet await Utils.AssertUntilAsync( @@ -419,8 +419,8 @@ await Utils.AssertUntilAsync( await Api.Instance.SetProviderAsync(client.GetMetadata().Name!, clientProvider); // now, send another event for the default handler - defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); - clientProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + await clientProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); // now the client should have received only the event from the named provider await Utils.AssertUntilAsync( @@ -479,7 +479,7 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); // send another event from the provider - this one should not be received - testProvider.SendEvent(ProviderEventTypes.ProviderReady); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderReady); // wait a bit and make sure we only have received the first event, but nothing after removing the event handler await Utils.AssertUntilAsync( diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index b4cb958c..9ca5b364 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -3,12 +3,14 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; using NSubstitute; using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; +using OpenFeature.Error; using OpenFeature.Model; using OpenFeature.Tests.Internal; using Xunit; @@ -35,18 +37,18 @@ public async Task Hooks_Should_Be_Called_In_Order() var providerHook = Substitute.For(); // Sequence - apiHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - clientHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - invocationHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - providerHook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - providerHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - invocationHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - clientHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - apiHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - providerHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - invocationHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - clientHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); - apiHook.Finally(Arg.Any>(), Arg.Any>()).Returns(Task.CompletedTask); + apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); var testProvider = new TestProvider(); testProvider.AddHook(providerHook); @@ -55,37 +57,37 @@ public async Task Hooks_Should_Be_Called_In_Order() var client = Api.Instance.GetClient(clientName, clientVersion); client.AddHooks(clientHook); - await client.GetBooleanValue(flagName, defaultValue, EvaluationContext.Empty, + await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empty, new FlagEvaluationOptions(invocationHook, ImmutableDictionary.Empty)); Received.InOrder(() => { - apiHook.Before(Arg.Any>(), Arg.Any>()); - clientHook.Before(Arg.Any>(), Arg.Any>()); - invocationHook.Before(Arg.Any>(), Arg.Any>()); - providerHook.Before(Arg.Any>(), Arg.Any>()); - providerHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - invocationHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - clientHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - apiHook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - providerHook.Finally(Arg.Any>(), Arg.Any>()); - invocationHook.Finally(Arg.Any>(), Arg.Any>()); - clientHook.Finally(Arg.Any>(), Arg.Any>()); - apiHook.Finally(Arg.Any>(), Arg.Any>()); + apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>()); }); - _ = apiHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = apiHook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = apiHook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); } [Fact] @@ -139,15 +141,15 @@ public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName), evaluationContext); - hook1.Before(Arg.Any>(), Arg.Any>()).Returns(evaluationContext); - hook2.Before(hookContext, Arg.Any>()).Returns(evaluationContext); + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(evaluationContext); + hook2.BeforeAsync(hookContext, Arg.Any>()).Returns(evaluationContext); var client = Api.Instance.GetClient("test", "1.0.0"); - await client.GetBooleanValue("test", false, EvaluationContext.Empty, + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, new FlagEvaluationOptions(ImmutableList.Create(hook1, hook2), ImmutableDictionary.Empty)); - _ = hook1.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = hook2.Received(1).Before(Arg.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), Arg.Any>()); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook2.Received(1).BeforeAsync(Arg.Is>(a => a.EvaluationContext.GetValue("test").AsString == "test"), Arg.Any>()); } [Fact] @@ -195,19 +197,19 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() provider.GetProviderHooks().Returns(ImmutableList.Empty); - provider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); + provider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); await Api.Instance.SetProviderAsync(provider); var hook = Substitute.For(); - hook.Before(Arg.Any>(), Arg.Any>()).Returns(hookContext); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(hookContext); var client = Api.Instance.GetClient("test", "1.0.0", null, clientContext); - await client.GetBooleanValue("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty)); + await client.GetBooleanValueAsync("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty)); // after proper merging, all properties should equal true - _ = provider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Is(y => + _ = provider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Is(y => (y.GetValue(propGlobal).AsBoolean ?? false) && (y.GetValue(propClient).AsBoolean ?? false) && (y.GetValue(propGlobalToOverwrite).AsBoolean ?? false) @@ -238,10 +240,10 @@ public async Task Hook_Should_Return_No_Errors() var hookContext = new HookContext("test", false, FlagValueType.Boolean, new ClientMetadata(null, null), new Metadata(null), EvaluationContext.Empty); - await hook.Before(hookContext, hookHints); - await hook.After(hookContext, new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"), hookHints); - await hook.Finally(hookContext, hookHints); - await hook.Error(hookContext, new Exception(), hookHints); + await hook.BeforeAsync(hookContext, hookHints); + await hook.AfterAsync(hookContext, new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"), hookHints); + await hook.FinallyAsync(hookContext, hookHints); + await hook.ErrorAsync(hookContext, new Exception(), hookHints); hookContext.ClientMetadata.Name.Should().BeNull(); hookContext.ClientMetadata.Version.Should().BeNull(); @@ -264,29 +266,29 @@ public async Task Hook_Should_Execute_In_Correct_Order() featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook.Before(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); - _ = hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.Finally(Arg.Any>(), Arg.Any>()); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook.Before(Arg.Any>(), Arg.Any>()); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Finally(Arg.Any>(), Arg.Any>()); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.FinallyAsync(Arg.Any>(), Arg.Any>()); }); - _ = hook.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = hook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.Received(1).Finally(Arg.Any>(), Arg.Any>()); - _ = featureProvider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -304,7 +306,7 @@ public async Task Register_Hooks_Should_Be_Available_At_All_Levels() await Api.Instance.SetProviderAsync(testProvider); var client = Api.Instance.GetClient(); client.AddHooks(hook2); - await client.GetBooleanValue("test", false, null, + await client.GetBooleanValueAsync("test", false, null, new FlagEvaluationOptions(hook3, ImmutableDictionary.Empty)); Assert.Single(Api.Instance.GetHooks()); @@ -324,39 +326,39 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook1.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - hook2.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); - hook2.After(Arg.Any>(), Arg.Any>(), null).Returns(Task.CompletedTask); - hook1.After(Arg.Any>(), Arg.Any>(), null).Returns(Task.CompletedTask); - hook2.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); - hook1.Finally(Arg.Any>(), null).Throws(new Exception()); + hook1.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + hook2.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook2.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask()); + hook1.FinallyAsync(Arg.Any>(), null).Throws(new Exception()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); client.GetHooks().Count().Should().Be(2); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook1.Before(Arg.Any>(), null); - hook2.Before(Arg.Any>(), null); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - hook2.After(Arg.Any>(), Arg.Any>(), null); - hook1.After(Arg.Any>(), Arg.Any>(), null); - hook2.Finally(Arg.Any>(), null); - hook1.Finally(Arg.Any>(), null); + hook1.BeforeAsync(Arg.Any>(), null); + hook2.BeforeAsync(Arg.Any>(), null); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null); + hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null); + hook2.FinallyAsync(Arg.Any>(), null); + hook1.FinallyAsync(Arg.Any>(), null); }); - _ = hook1.Received(1).Before(Arg.Any>(), null); - _ = hook2.Received(1).Before(Arg.Any>(), null); - _ = featureProvider.Received(1).ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - _ = hook2.Received(1).After(Arg.Any>(), Arg.Any>(), null); - _ = hook1.Received(1).After(Arg.Any>(), Arg.Any>(), null); - _ = hook2.Received(1).Finally(Arg.Any>(), null); - _ = hook1.Received(1).Finally(Arg.Any>(), null); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); + _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + _ = hook2.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook1.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook2.Received(1).FinallyAsync(Arg.Any>(), null); + _ = hook1.Received(1).FinallyAsync(Arg.Any>(), null); } [Fact] @@ -371,31 +373,31 @@ public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() featureProvider1.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook1.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - hook2.Before(Arg.Any>(), null).Returns(EvaluationContext.Empty); - featureProvider1.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()).Throws(new Exception()); - hook2.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); - hook1.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); + 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()); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider1); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook1.Before(Arg.Any>(), null); - hook2.Before(Arg.Any>(), null); - featureProvider1.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()); - hook2.Error(Arg.Any>(), Arg.Any(), null); - hook1.Error(Arg.Any>(), Arg.Any(), null); + hook1.BeforeAsync(Arg.Any>(), null); + hook2.BeforeAsync(Arg.Any>(), null); + featureProvider1.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); }); - _ = hook1.Received(1).Before(Arg.Any>(), null); - _ = hook2.Received(1).Before(Arg.Any>(), null); - _ = hook1.Received(1).Error(Arg.Any>(), Arg.Any(), null); - _ = hook2.Received(1).Error(Arg.Any>(), Arg.Any(), null); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); } [Fact] @@ -410,27 +412,27 @@ public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_ featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); // Sequence - hook1.Before(Arg.Any>(), Arg.Any>()).ThrowsAsync(new Exception()); - _ = hook1.Error(Arg.Any>(), Arg.Any(), null); - _ = hook2.Error(Arg.Any>(), Arg.Any(), null); + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(new Exception()); + _ = hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); - await client.GetBooleanValue("test", false); + await client.GetBooleanValueAsync("test", false); Received.InOrder(() => { - hook1.Before(Arg.Any>(), Arg.Any>()); - hook2.Error(Arg.Any>(), Arg.Any(), null); - hook1.Error(Arg.Any>(), Arg.Any(), null); + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()); + hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); }); - _ = hook1.Received(1).Before(Arg.Any>(), Arg.Any>()); - _ = hook2.DidNotReceive().Before(Arg.Any>(), Arg.Any>()); - _ = hook1.Received(1).Error(Arg.Any>(), Arg.Any(), null); - _ = hook2.Received(1).Error(Arg.Any>(), Arg.Any(), null); + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook2.DidNotReceive().BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); } [Fact] @@ -447,29 +449,29 @@ public async Task Hook_Hints_May_Be_Optional() featureProvider.GetProviderHooks() .Returns(ImmutableList.Empty); - hook.Before(Arg.Any>(), Arg.Any>()) + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) .Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue("test", false, Arg.Any()) + featureProvider.ResolveBooleanValueAsync("test", false, Arg.Any()) .Returns(new ResolutionDetails("test", false)); - hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .Returns(Task.FromResult(Task.CompletedTask)); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); - hook.Finally(Arg.Any>(), Arg.Any>()) - .Returns(Task.CompletedTask); + hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); - await client.GetBooleanValue("test", false, EvaluationContext.Empty, flagOptions); + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, flagOptions); Received.InOrder(() => { - hook.Received().Before(Arg.Any>(), Arg.Any>()); - featureProvider.Received().ResolveBooleanValue("test", false, Arg.Any()); - hook.Received().After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received().Finally(Arg.Any>(), Arg.Any>()); + hook.Received().BeforeAsync(Arg.Any>(), Arg.Any>()); + featureProvider.Received().ResolveBooleanValueAsync("test", false, Arg.Any()); + hook.Received().AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>()); }); } @@ -485,26 +487,26 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() featureProvider.GetMetadata().Returns(new Metadata(null)); // Sequence - hook.Before(Arg.Any>(), Arg.Any>()).ThrowsAsync(exceptionToThrow); - hook.Error(Arg.Any>(), Arg.Any(), null).Returns(Task.CompletedTask); - hook.Finally(Arg.Any>(), null).Returns(Task.CompletedTask); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(exceptionToThrow); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); + hook.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask()); var client = Api.Instance.GetClient(); client.AddHooks(hook); - var resolvedFlag = await client.GetBooleanValue("test", true); + var resolvedFlag = await client.GetBooleanValueAsync("test", true); Received.InOrder(() => { - hook.Before(Arg.Any>(), Arg.Any>()); - hook.Error(Arg.Any>(), Arg.Any(), null); - hook.Finally(Arg.Any>(), null); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook.FinallyAsync(Arg.Any>(), null); }); resolvedFlag.Should().BeTrue(); - _ = hook.Received(1).Before(Arg.Any>(), null); - _ = hook.Received(1).Error(Arg.Any>(), exceptionToThrow, null); - _ = hook.Received(1).Finally(Arg.Any>(), null); + _ = hook.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook.Received(1).ErrorAsync(Arg.Any>(), exceptionToThrow, null); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), null); } [Fact] @@ -522,36 +524,101 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() featureProvider.GetProviderHooks() .Returns(ImmutableList.Empty); - hook.Before(Arg.Any>(), Arg.Any>()) + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) .Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValue(Arg.Any(), Arg.Any(), Arg.Any()) + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new ResolutionDetails("test", false)); - hook.After(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .ThrowsAsync(exceptionToThrow); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Throws(exceptionToThrow); - hook.Error(Arg.Any>(), Arg.Any(), Arg.Any>()) - .Returns(Task.CompletedTask); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) + .Returns(new ValueTask()); - hook.Finally(Arg.Any>(), Arg.Any>()) - .Returns(Task.CompletedTask); + hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); - var resolvedFlag = await client.GetBooleanValue("test", true, config: flagOptions); + var resolvedFlag = await client.GetBooleanValueAsync("test", true, config: flagOptions); resolvedFlag.Should().BeTrue(); Received.InOrder(() => { - hook.Received(1).Before(Arg.Any>(), Arg.Any>()); - hook.Received(1).After(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received(1).Finally(Arg.Any>(), Arg.Any>()); + hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); }); - await featureProvider.DidNotReceive().ResolveBooleanValue("test", false, Arg.Any()); + await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); + } + + [Fact] + public async Task Successful_Resolution_Should_Pass_Cancellation_Token() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var cts = new CancellationTokenSource(); + + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + + hook.BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any(), cts.Token).Returns(new ResolutionDetails("test", false)); + _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + client.AddHooks(hook); + + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, null, cts.Token); + + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + } + + [Fact] + public async Task Failed_Resolution_Should_Pass_Cancellation_Token() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var flagOptions = new FlagEvaluationOptions(hook); + var exceptionToThrow = new GeneralException("Fake Exception"); + var cts = new CancellationTokenSource(); + + featureProvider.GetMetadata() + .Returns(new Metadata(null)); + + featureProvider.GetProviderHooks() + .Returns(ImmutableList.Empty); + + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty); + + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Throws(exceptionToThrow); + + hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) + .Returns(new ValueTask()); + + hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); + + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + + await client.GetBooleanValueAsync("test", true, EvaluationContext.Empty, flagOptions, cts.Token); + + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + + await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index c34a013d..673c183d 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -32,13 +31,13 @@ public async Task OpenFeature_Should_Initialize_Provider() providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerMockDefault); - await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()); + await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerMockNamed = Substitute.For(); providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("the-name", providerMockNamed); - await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext()); + await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()); } [Fact] @@ -50,27 +49,27 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() providerA.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerA); - await providerA.Received(1).Initialize(Api.Instance.GetContext()); + await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerB = Substitute.For(); providerB.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerB); - await providerB.Received(1).Initialize(Api.Instance.GetContext()); - await providerA.Received(1).Shutdown(); + await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()); + await providerA.Received(1).ShutdownAsync(); var providerC = Substitute.For(); providerC.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("named", providerC); - await providerC.Received(1).Initialize(Api.Instance.GetContext()); + await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerD = Substitute.For(); providerD.GetStatus().Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("named", providerD); - await providerD.Received(1).Initialize(Api.Instance.GetContext()); - await providerC.Received(1).Shutdown(); + await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()); + await providerC.Received(1).ShutdownAsync(); } [Fact] @@ -86,10 +85,10 @@ public async Task OpenFeature_Should_Support_Shutdown() await Api.Instance.SetProviderAsync(providerA); await Api.Instance.SetProviderAsync("named", providerB); - await Api.Instance.Shutdown(); + await Api.Instance.ShutdownAsync(); - await providerA.Received(1).Shutdown(); - await providerB.Received(1).Shutdown(); + await providerA.Received(1).ShutdownAsync(); + await providerB.Received(1).ShutdownAsync(); } [Fact] @@ -128,8 +127,8 @@ public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() const string name = "new-client"; var openFeature = Api.Instance; - await openFeature.SetProviderAsync(name, new TestProvider()).ConfigureAwait(true); - await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()).ConfigureAwait(true); + await openFeature.SetProviderAsync(name, new TestProvider()); + await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); } @@ -141,8 +140,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instanc var openFeature = Api.Instance; var provider = new TestProvider(); - await openFeature.SetProviderAsync("a", provider).ConfigureAwait(true); - await openFeature.SetProviderAsync("b", provider).ConfigureAwait(true); + await openFeature.SetProviderAsync("a", provider); + await openFeature.SetProviderAsync("b", provider); var clientA = openFeature.GetProvider("a"); var clientB = openFeature.GetProvider("b"); @@ -233,8 +232,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() { var openFeature = Api.Instance; - await openFeature.SetProviderAsync("client1", new TestProvider()).ConfigureAwait(true); - await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()).ConfigureAwait(true); + await openFeature.SetProviderAsync("client1", new TestProvider()); + await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()); var client1 = openFeature.GetClient("client1"); var client2 = openFeature.GetClient("client2"); @@ -242,19 +241,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() client1.GetMetadata().Name.Should().Be("client1"); client2.GetMetadata().Name.Should().Be("client2"); - (await client1.GetBooleanValue("test", false)).Should().BeTrue(); - (await client2.GetBooleanValue("test", false)).Should().BeFalse(); - } - - [Fact] - public async Task SetProviderAsync_Should_Throw_When_Null_ClientName() - { - var openFeature = Api.Instance; - - var exception = await Assert.ThrowsAsync(() => openFeature.SetProviderAsync(null!, new TestProvider())); - - exception.Should().BeOfType(); - exception.ParamName.Should().Be("clientName"); + (await client1.GetBooleanValueAsync("test", false)).Should().BeTrue(); + (await client2.GetBooleanValueAsync("test", false)).Should().BeFalse(); } } } diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 0b25ebfa..ccec89bd 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -21,19 +21,19 @@ public async Task Default_Provider_Is_Set_Without_Await() var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider, context); + await repository.SetProviderAsync(provider, context); Assert.Equal(provider, repository.GetProvider()); } [Fact] - public async void AfterSet_Is_Invoked_For_Setting_Default_Provider() + public async Task AfterSet_Is_Invoked_For_Setting_Default_Provider() { var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProvider(provider, context, afterSet: (theProvider) => + await repository.SetProviderAsync(provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -48,9 +48,9 @@ public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Default_ var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(providerMock, context); - providerMock.Received(1).Initialize(context); - providerMock.DidNotReceive().Shutdown(); + await repository.SetProviderAsync(providerMock, context); + providerMock.Received(1).InitializeAsync(context); + providerMock.DidNotReceive().ShutdownAsync(); } [Fact] @@ -61,7 +61,7 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Default_Provider() providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider(providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync(providerMock, context, afterInitialization: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -76,10 +76,10 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provide var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); + providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; Exception? receivedError = null; - await repository.SetProvider(providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync(providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -99,8 +99,8 @@ public async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus sta var providerMock = Substitute.For(); providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(providerMock, context); - providerMock.DidNotReceive().Initialize(context); + await repository.SetProviderAsync(providerMock, context); + providerMock.DidNotReceive().InitializeAsync(context); } [Theory] @@ -114,7 +114,7 @@ public async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatu providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider(providerMock, context, afterInitialization: provider => { callCount++; }); + await repository.SetProviderAsync(providerMock, context, afterInitialization: provider => { callCount++; }); Assert.Equal(0, callCount); } @@ -129,10 +129,10 @@ public async Task Replaced_Default_Provider_Is_Shutdown() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider(provider2, context); - provider1.Received(1).Shutdown(); - provider2.DidNotReceive().Shutdown(); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync(provider2, context); + provider1.Received(1).ShutdownAsync(); + provider2.DidNotReceive().ShutdownAsync(); } [Fact] @@ -146,9 +146,9 @@ public async Task AfterShutdown_Is_Called_For_Shutdown_Provider() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); + await repository.SetProviderAsync(provider1, context); var callCount = 0; - await repository.SetProvider(provider2, context, afterShutdown: provider => + await repository.SetProviderAsync(provider2, context, afterShutdown: provider => { Assert.Equal(provider, provider1); callCount++; @@ -161,17 +161,17 @@ public async Task AfterError_Is_Called_For_Shutdown_That_Throws() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR")); + provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); provider1.GetStatus().Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); + await repository.SetProviderAsync(provider1, context); var callCount = 0; Exception? errorThrown = null; - await repository.SetProvider(provider2, context, afterError: (provider, ex) => + await repository.SetProviderAsync(provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); errorThrown = ex; @@ -187,7 +187,8 @@ public async Task Named_Provider_Provider_Is_Set_Without_Await() var repository = new ProviderRepository(); var provider = new NoOpFeatureProvider(); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", provider, context); + + await repository.SetProviderAsync("the-name", provider, context); Assert.Equal(provider, repository.GetProvider("the-name")); } @@ -199,7 +200,7 @@ public async Task AfterSet_Is_Invoked_For_Setting_Named_Provider() var context = new EvaluationContextBuilder().Build(); var callCount = 0; // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProvider("the-name", provider, context, afterSet: (theProvider) => + await repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => { callCount++; Assert.Equal(provider, theProvider); @@ -214,9 +215,9 @@ public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Named_Pr var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", providerMock, context); - providerMock.Received(1).Initialize(context); - providerMock.DidNotReceive().Shutdown(); + await repository.SetProviderAsync("the-name", providerMock, context); + providerMock.Received(1).InitializeAsync(context); + providerMock.DidNotReceive().ShutdownAsync(); } [Fact] @@ -227,7 +228,7 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Named_Provider() providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider("the-name", providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync("the-name", providerMock, context, afterInitialization: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -242,10 +243,10 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider( var providerMock = Substitute.For(); providerMock.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - providerMock.When(x => x.Initialize(context)).Throw(new Exception("BAD THINGS")); + providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; Exception? receivedError = null; - await repository.SetProvider("the-provider", providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync("the-provider", providerMock, context, afterError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; @@ -265,8 +266,8 @@ public async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStat var providerMock = Substitute.For(); providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", providerMock, context); - providerMock.DidNotReceive().Initialize(context); + await repository.SetProviderAsync("the-name", providerMock, context); + providerMock.DidNotReceive().InitializeAsync(context); } [Theory] @@ -280,7 +281,7 @@ public async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(Provide providerMock.GetStatus().Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProvider("the-name", providerMock, context, + await repository.SetProviderAsync("the-name", providerMock, context, afterInitialization: provider => { callCount++; }); Assert.Equal(0, callCount); } @@ -296,10 +297,10 @@ public async Task Replaced_Named_Provider_Is_Shutdown() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", provider1, context); - await repository.SetProvider("the-name", provider2, context); - provider1.Received(1).Shutdown(); - provider2.DidNotReceive().Shutdown(); + await repository.SetProviderAsync("the-name", provider1, context); + await repository.SetProviderAsync("the-name", provider2, context); + provider1.Received(1).ShutdownAsync(); + provider2.DidNotReceive().ShutdownAsync(); } [Fact] @@ -313,9 +314,9 @@ public async Task AfterShutdown_Is_Called_For_Shutdown_Named_Provider() provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-provider", provider1, context); + await repository.SetProviderAsync("the-provider", provider1, context); var callCount = 0; - await repository.SetProvider("the-provider", provider2, context, afterShutdown: provider => + await repository.SetProviderAsync("the-provider", provider2, context, afterShutdown: provider => { Assert.Equal(provider, provider1); callCount++; @@ -328,17 +329,17 @@ public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR")); + provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); provider1.GetStatus().Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); provider2.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("the-name", provider1, context); + await repository.SetProviderAsync("the-name", provider1, context); var callCount = 0; Exception? errorThrown = null; - await repository.SetProvider("the-name", provider2, context, afterError: (provider, ex) => + await repository.SetProviderAsync("the-name", provider2, context, afterError: (provider, ex) => { Assert.Equal(provider, provider1); errorThrown = ex; @@ -360,12 +361,12 @@ public async Task In_Use_Provider_Named_And_Default_Is_Not_Shutdown() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider("A", provider1, context); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("A", provider1, context); // Provider one is replaced for "A", but not default. - await repository.SetProvider("A", provider2, context); + await repository.SetProviderAsync("A", provider2, context); - provider1.DidNotReceive().Shutdown(); + provider1.DidNotReceive().ShutdownAsync(); } [Fact] @@ -380,12 +381,12 @@ public async Task In_Use_Provider_Two_Named_Is_Not_Shutdown() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("B", provider1, context); - await repository.SetProvider("A", provider1, context); + await repository.SetProviderAsync("B", provider1, context); + await repository.SetProviderAsync("A", provider1, context); // Provider one is replaced for "A", but not "B". - await repository.SetProvider("A", provider2, context); + await repository.SetProviderAsync("A", provider2, context); - provider1.DidNotReceive().Shutdown(); + provider1.DidNotReceive().ShutdownAsync(); } [Fact] @@ -400,13 +401,13 @@ public async Task When_All_Instances_Are_Removed_Shutdown_Is_Called() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("B", provider1, context); - await repository.SetProvider("A", provider1, context); + await repository.SetProviderAsync("B", provider1, context); + await repository.SetProviderAsync("A", provider1, context); - await repository.SetProvider("A", provider2, context); - await repository.SetProvider("B", provider2, context); + await repository.SetProviderAsync("A", provider2, context); + await repository.SetProviderAsync("B", provider2, context); - provider1.Received(1).Shutdown(); + provider1.Received(1).ShutdownAsync(); } [Fact] @@ -421,8 +422,8 @@ public async Task Can_Get_Providers_By_Name() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("A", provider1, context); - await repository.SetProvider("B", provider2, context); + await repository.SetProviderAsync("A", provider1, context); + await repository.SetProviderAsync("B", provider2, context); Assert.Equal(provider1, repository.GetProvider("A")); Assert.Equal(provider2, repository.GetProvider("B")); @@ -440,8 +441,8 @@ public async Task Replaced_Named_Provider_Gets_Latest_Set() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider("A", provider1, context); - await repository.SetProvider("A", provider2, context); + await repository.SetProviderAsync("A", provider1, context); + await repository.SetProviderAsync("A", provider2, context); Assert.Equal(provider2, repository.GetProvider("A")); } @@ -461,17 +462,17 @@ public async Task Can_Shutdown_All_Providers() var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider("provider1", provider1, context); - await repository.SetProvider("provider2", provider2, context); - await repository.SetProvider("provider2a", provider2, context); - await repository.SetProvider("provider3", provider3, context); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("provider1", provider1, context); + await repository.SetProviderAsync("provider2", provider2, context); + await repository.SetProviderAsync("provider2a", provider2, context); + await repository.SetProviderAsync("provider3", provider3, context); - await repository.Shutdown(); + await repository.ShutdownAsync(); - provider1.Received(1).Shutdown(); - provider2.Received(1).Shutdown(); - provider3.Received(1).Shutdown(); + provider1.Received(1).ShutdownAsync(); + provider2.Received(1).ShutdownAsync(); + provider3.Received(1).ShutdownAsync(); } [Fact] @@ -480,27 +481,27 @@ public async Task Errors_During_Shutdown_Propagate() var repository = new ProviderRepository(); var provider1 = Substitute.For(); provider1.GetStatus().Returns(ProviderStatus.NotReady); - provider1.Shutdown().Throws(new Exception("SHUTDOWN ERROR 1")); + provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 1")); var provider2 = Substitute.For(); provider2.GetStatus().Returns(ProviderStatus.NotReady); - provider2.Shutdown().Throws(new Exception("SHUTDOWN ERROR 2")); + provider2.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 2")); var provider3 = Substitute.For(); provider3.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider1, context); - await repository.SetProvider("provider1", provider1, context); - await repository.SetProvider("provider2", provider2, context); - await repository.SetProvider("provider2a", provider2, context); - await repository.SetProvider("provider3", provider3, context); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("provider1", provider1, context); + await repository.SetProviderAsync("provider2", provider2, context); + await repository.SetProviderAsync("provider2a", provider2, context); + await repository.SetProviderAsync("provider3", provider3, context); var callCountShutdown1 = 0; var callCountShutdown2 = 0; var totalCallCount = 0; - await repository.Shutdown(afterError: (provider, exception) => + await repository.ShutdownAsync(afterError: (provider, exception) => { totalCallCount++; if (provider == provider1) @@ -519,9 +520,9 @@ await repository.Shutdown(afterError: (provider, exception) => Assert.Equal(1, callCountShutdown1); Assert.Equal(1, callCountShutdown2); - provider1.Received(1).Shutdown(); - provider2.Received(1).Shutdown(); - provider3.Received(1).Shutdown(); + provider1.Received(1).ShutdownAsync(); + provider2.Received(1).ShutdownAsync(); + provider3.Received(1).ShutdownAsync(); } [Fact] @@ -531,12 +532,12 @@ public async Task Setting_Same_Default_Provider_Has_No_Effect() var provider = Substitute.For(); provider.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider, context); - await repository.SetProvider(provider, context); + await repository.SetProviderAsync(provider, context); + await repository.SetProviderAsync(provider, context); Assert.Equal(provider, repository.GetProvider()); - provider.Received(1).Initialize(context); - provider.DidNotReceive().Shutdown(); + provider.Received(1).InitializeAsync(context); + provider.DidNotReceive().ShutdownAsync(); } [Fact] @@ -546,12 +547,12 @@ public async Task Setting_Null_Default_Provider_Has_No_Effect() var provider = Substitute.For(); provider.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(provider, context); - await repository.SetProvider(null, context); + await repository.SetProviderAsync(provider, context); + await repository.SetProviderAsync(null, context); Assert.Equal(provider, repository.GetProvider()); - provider.Received(1).Initialize(context); - provider.DidNotReceive().Shutdown(); + provider.Received(1).InitializeAsync(context); + provider.DidNotReceive().ShutdownAsync(); } [Fact] @@ -566,33 +567,12 @@ public async Task Setting_Null_Named_Provider_Removes_It() defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); - await repository.SetProvider(defaultProvider, context); + await repository.SetProviderAsync(defaultProvider, context); - await repository.SetProvider("named-provider", namedProvider, context); - await repository.SetProvider("named-provider", null, context); + await repository.SetProviderAsync("named-provider", namedProvider, context); + await repository.SetProviderAsync("named-provider", null, context); Assert.Equal(defaultProvider, repository.GetProvider("named-provider")); } - - [Fact] - public async Task Setting_Named_Provider_With_Null_Name_Has_No_Effect() - { - var repository = new ProviderRepository(); - var context = new EvaluationContextBuilder().Build(); - - var defaultProvider = Substitute.For(); - defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); - await repository.SetProvider(defaultProvider, context); - - var namedProvider = Substitute.For(); - namedProvider.GetStatus().Returns(ProviderStatus.NotReady); - - await repository.SetProvider(null, namedProvider, context); - - namedProvider.DidNotReceive().Initialize(context); - namedProvider.DidNotReceive().Shutdown(); - - Assert.Equal(defaultProvider, repository.GetProvider(null)); - } } } diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 64e1df46..83974c23 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Error; using OpenFeature.Model; @@ -109,45 +110,45 @@ public InMemoryProviderTests() } [Fact] - public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() + public async Task GetBoolean_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveBooleanValueAsync("boolean-flag", false, EvaluationContext.Empty); Assert.True(details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("on", details.Variant); } [Fact] - public async void GetString_ShouldEvaluateWithReasonAndVariant() + public async Task GetString_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("string-flag", "nope", EvaluationContext.Empty); Assert.Equal("hi", details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("greeting", details.Variant); } [Fact] - public async void GetInt_ShouldEvaluateWithReasonAndVariant() + public async Task GetInt_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveIntegerValueAsync("integer-flag", 13, EvaluationContext.Empty); Assert.Equal(10, details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("ten", details.Variant); } [Fact] - public async void GetDouble_ShouldEvaluateWithReasonAndVariant() + public async Task GetDouble_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveDoubleValueAsync("float-flag", 13, EvaluationContext.Empty); Assert.Equal(0.5, details.Value); Assert.Equal(Reason.Static, details.Reason); Assert.Equal("half", details.Variant); } [Fact] - public async void GetStruct_ShouldEvaluateWithReasonAndVariant() + public async Task GetStruct_ShouldEvaluateWithReasonAndVariant() { - ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty); + ResolutionDetails details = await this.commonProvider.ResolveStructureValueAsync("object-flag", new Value(), EvaluationContext.Empty); Assert.Equal(true, details.Value.AsStructure?["showImages"].AsBoolean); Assert.Equal("Check out these pics!", details.Value.AsStructure?["title"].AsString); Assert.Equal(100, details.Value.AsStructure?["imagesPerPage"].AsInteger); @@ -156,17 +157,17 @@ public async void GetStruct_ShouldEvaluateWithReasonAndVariant() } [Fact] - public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() + public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() { EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); - ResolutionDetails details = await this.commonProvider.ResolveStringValue("context-aware", "nope", context); + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("context-aware", "nope", context); Assert.Equal("INTERNAL", details.Value); Assert.Equal(Reason.TargetingMatch, details.Reason); Assert.Equal("internal", details.Variant); } [Fact] - public async void EmptyFlags_ShouldWork() + public async Task EmptyFlags_ShouldWork() { var provider = new InMemoryProvider(); await provider.UpdateFlags(); @@ -174,31 +175,31 @@ public async void EmptyFlags_ShouldWork() } [Fact] - public async void MissingFlag_ShouldThrow() + public async Task MissingFlag_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("missing-flag", false, EvaluationContext.Empty)); } [Fact] - public async void MismatchedFlag_ShouldThrow() + public async Task MismatchedFlag_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveStringValueAsync("boolean-flag", "nope", EvaluationContext.Empty)); } [Fact] - public async void MissingDefaultVariant_ShouldThrow() + public async Task MissingDefaultVariant_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-flag", false, EvaluationContext.Empty)); } [Fact] - public async void MissingEvaluatedVariant_ShouldThrow() + public async Task MissingEvaluatedVariant_ShouldThrow() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-evaluator-flag", false, EvaluationContext.Empty)); } [Fact] - public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() + public async Task PutConfiguration_shouldUpdateConfigAndRunHandlers() { var provider = new InMemoryProvider(new Dictionary(){ { @@ -211,7 +212,7 @@ public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() ) }}); - ResolutionDetails details = await provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty); + ResolutionDetails details = await provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty); Assert.True(details.Value); // update flags @@ -229,10 +230,10 @@ await provider.UpdateFlags(new Dictionary(){ var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res?.Type); - await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)); + await Assert.ThrowsAsync(() => provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty)); // new flag should be present, old gone (defaults), handler run. - ResolutionDetails detailsAfter = await provider.ResolveStringValue("new-flag", "nope", EvaluationContext.Empty); + ResolutionDetails detailsAfter = await provider.ResolveStringValueAsync("new-flag", "nope", EvaluationContext.Empty); Assert.True(details.Value); Assert.Equal("hi", detailsAfter.Value); } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index cdb59a0c..c949b373 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Threading; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; @@ -11,25 +12,25 @@ public class TestHookNoOverride : Hook { } public class TestHook : Hook { - public override Task Before(HookContext context, IReadOnlyDictionary? hints = null) + public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.FromResult(EvaluationContext.Empty); + return new ValueTask(EvaluationContext.Empty); } - public override Task After(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null) + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } - public override Task Error(HookContext context, Exception error, IReadOnlyDictionary? hints = null) + public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } - public override Task Finally(HookContext context, IReadOnlyDictionary? hints = null) + public override ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - return Task.CompletedTask; + return new ValueTask(); } } @@ -64,32 +65,32 @@ public override Metadata GetMetadata() return new Metadata(this.Name); } - public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext? context = null) + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); } - public override Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext? context = null) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext? context = null) + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, - EvaluationContext? context = null) + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } @@ -104,16 +105,16 @@ public void SetStatus(ProviderStatus status) this._status = status; } - public override Task Initialize(EvaluationContext context) + public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { this._status = ProviderStatus.Ready; - this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }); - return base.Initialize(context); + await this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }, cancellationToken).ConfigureAwait(false); + await base.InitializeAsync(context, cancellationToken).ConfigureAwait(false); } - internal void SendEvent(ProviderEventTypes eventType) + internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) { - this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name }); + return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name }, cancellationToken); } } } diff --git a/test/OpenFeature.Tests/TestUtilsTest.cs b/test/OpenFeature.Tests/TestUtilsTest.cs index 1d0882b0..b65a91f5 100644 --- a/test/OpenFeature.Tests/TestUtilsTest.cs +++ b/test/OpenFeature.Tests/TestUtilsTest.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using Xunit; namespace OpenFeature.Tests @@ -8,13 +9,13 @@ namespace OpenFeature.Tests public class TestUtilsTest { [Fact] - public async void Should_Fail_If_Assertion_Fails() + public async Task Should_Fail_If_Assertion_Fails() { await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)); } [Fact] - public async void Should_Pass_If_Assertion_Fails() + public async Task Should_Pass_If_Assertion_Fails() { await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))); } From a7b6d8561716763f324325a8803b913c4d69c044 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:08:38 +1000 Subject: [PATCH 181/316] chore(deps): update xunit-dotnet monorepo to v2.8.1 (#266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [xunit](https://togithub.com/xunit/xunit) | `2.7.1` -> `2.8.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit/2.7.1/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit/2.7.1/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [xunit.runner.visualstudio](https://togithub.com/xunit/visualstudio.xunit) | `2.5.8` -> `2.8.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit.runner.visualstudio/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit.runner.visualstudio/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit.runner.visualstudio/2.5.8/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit.runner.visualstudio/2.5.8/2.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
xunit/xunit (xunit) ### [`v2.8.1`](https://togithub.com/xunit/xunit/compare/2.8.0...2.8.1) [Compare Source](https://togithub.com/xunit/xunit/compare/2.8.0...2.8.1) ### [`v2.8.0`](https://togithub.com/xunit/xunit/compare/2.7.1...2.8.0) [Compare Source](https://togithub.com/xunit/xunit/compare/2.7.1...2.8.0)
xunit/visualstudio.xunit (xunit.runner.visualstudio) ### [`v2.8.1`](https://togithub.com/xunit/visualstudio.xunit/compare/2.8.0...2.8.1) [Compare Source](https://togithub.com/xunit/visualstudio.xunit/compare/2.8.0...2.8.1) ### [`v2.8.0`](https://togithub.com/xunit/visualstudio.xunit/compare/2.5.8...2.8.0) [Compare Source](https://togithub.com/xunit/visualstudio.xunit/compare/2.5.8...2.8.0)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. πŸ”• **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 757f24c9..b8b4dce7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,8 +24,8 @@ - - + + From 281295d2999e4d36c5a2078cbfdfe5e59f4652b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:20:51 +1000 Subject: [PATCH 182/316] chore(deps): update codecov/codecov-action action to v4.5.0 (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [codecov/codecov-action](https://togithub.com/codecov/codecov-action) | action | minor | `v4.3.1` -> `v4.5.0` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v4.5.0`](https://togithub.com/codecov/codecov-action/compare/v4.4.1...v4.5.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.4.1...v4.5.0) ### [`v4.4.1`](https://togithub.com/codecov/codecov-action/releases/tag/v4.4.1) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.4.0...v4.4.1) #### What's Changed - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 7.8.0 to 7.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1427](https://togithub.com/codecov/codecov-action/pull/1427) - fix: prevent xlarge from running on forks by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1432](https://togithub.com/codecov/codecov-action/pull/1432) - build(deps): bump github/codeql-action from 3.25.4 to 3.25.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1439](https://togithub.com/codecov/codecov-action/pull/1439) - build(deps): bump actions/checkout from 4.1.5 to 4.1.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1438](https://togithub.com/codecov/codecov-action/pull/1438) - fix: isPullRequestFromFork returns false for any PR by [@​shahar-h](https://togithub.com/shahar-h) in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) - chore(release): 4.4.1 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1441](https://togithub.com/codecov/codecov-action/pull/1441) #### New Contributors - [@​shahar-h](https://togithub.com/shahar-h) made their first contribution in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.4.0...v4.4.1 #### What's Changed - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 7.8.0 to 7.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1427](https://togithub.com/codecov/codecov-action/pull/1427) - fix: prevent xlarge from running on forks by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1432](https://togithub.com/codecov/codecov-action/pull/1432) - build(deps): bump github/codeql-action from 3.25.4 to 3.25.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1439](https://togithub.com/codecov/codecov-action/pull/1439) - build(deps): bump actions/checkout from 4.1.5 to 4.1.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1438](https://togithub.com/codecov/codecov-action/pull/1438) - fix: isPullRequestFromFork returns false for any PR by [@​shahar-h](https://togithub.com/shahar-h) in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) - chore(release): 4.4.1 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1441](https://togithub.com/codecov/codecov-action/pull/1441) #### New Contributors - [@​shahar-h](https://togithub.com/shahar-h) made their first contribution in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.4.0...v4.4.1 ### [`v4.4.0`](https://togithub.com/codecov/codecov-action/releases/tag/v4.4.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.3.1...v4.4.0) #### What's Changed - chore: Clarify isPullRequestFromFork by [@​jsoref](https://togithub.com/jsoref) in [https://github.com/codecov/codecov-action/pull/1411](https://togithub.com/codecov/codecov-action/pull/1411) - build(deps): bump actions/checkout from 4.1.4 to 4.1.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1423](https://togithub.com/codecov/codecov-action/pull/1423) - build(deps): bump github/codeql-action from 3.25.3 to 3.25.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1421](https://togithub.com/codecov/codecov-action/pull/1421) - build(deps): bump ossf/scorecard-action from 2.3.1 to 2.3.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1420](https://togithub.com/codecov/codecov-action/pull/1420) - feat: remove GPG and run on spawn by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1426](https://togithub.com/codecov/codecov-action/pull/1426) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 7.8.0 to 7.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1428](https://togithub.com/codecov/codecov-action/pull/1428) - chore(release): 4.4.0 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1430](https://togithub.com/codecov/codecov-action/pull/1430) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.3.1...v4.4.0
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](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 010ed660..1f07ffc6 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -37,7 +37,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v4.3.1 + - uses: codecov/codecov-action@v4.5.0 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From 46c2b153c848bd3a500b828ddb89bd3b07753bf1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:25:40 +1000 Subject: [PATCH 183/316] chore(deps): update dependency githubactionstestlogger to v2.4.1 (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [GitHubActionsTestLogger](https://togithub.com/Tyrrrz/GitHubActionsTestLogger) | `2.3.3` -> `2.4.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/GitHubActionsTestLogger/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/GitHubActionsTestLogger/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/GitHubActionsTestLogger/2.3.3/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/GitHubActionsTestLogger/2.3.3/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
Tyrrrz/GitHubActionsTestLogger (GitHubActionsTestLogger) ### [`v2.4.1`](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/releases/tag/2.4.1) [Compare Source](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/compare/2.4...2.4.1) #### What's Changed - Fix incorrect fallback for the "include not found tests" option by [@​Tyrrrz](https://togithub.com/Tyrrrz) in [https://github.com/Tyrrrz/GitHubActionsTestLogger/pull/27](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/pull/27) **Full Changelog**: https://github.com/Tyrrrz/GitHubActionsTestLogger/compare/2.4...2.4.1 ### [`v2.4.0`](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/compare/2.3.3...2.4) [Compare Source](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/compare/2.3.3...2.4)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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 has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b8b4dce7..3ceb087b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + From 63faa8440cd650b0bd6c3ec009ad9bd78bc31f32 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 3 Jul 2024 15:47:50 -0400 Subject: [PATCH 184/316] feat!: internally maintain provider status (#276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR implements a few things from spec 0.8.0: - implements internal provider status (already implemented in JS) - the provider no longer updates its status to READY/ERROR, etc after init (the SDK does this automatically) - the provider's state is updated according to the last event it fired - adds `PROVIDER_FATAL` error and code - adds "short circuit" feature when evaluations are skipped if provider is `NOT_READY` or `FATAL` - removes some deprecations that were making the work harder since we already have pending breaking changes. Fixes: https://github.com/open-feature/dotnet-sdk/issues/250 --------- Signed-off-by: Todd Baert Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- src/OpenFeature/Api.cs | 44 ++- src/OpenFeature/Constant/ErrorType.cs | 5 + src/OpenFeature/Constant/ProviderStatus.cs | 7 +- .../Error/ProviderFatalException.cs | 23 ++ .../Error/ProviderNotReadyException.cs | 2 +- src/OpenFeature/EventExecutor.cs | 21 +- src/OpenFeature/FeatureProvider.cs | 24 +- src/OpenFeature/IFeatureClient.cs | 7 + src/OpenFeature/Model/ProviderEvents.cs | 5 + src/OpenFeature/OpenFeatureClient.cs | 18 +- src/OpenFeature/ProviderRepository.cs | 100 +++---- .../OpenFeatureClientTests.cs | 94 +++++- .../OpenFeatureEventTests.cs | 25 +- test/OpenFeature.Tests/OpenFeatureTests.cs | 16 +- .../ProviderRepositoryTests.cs | 269 ++++-------------- test/OpenFeature.Tests/TestImplementations.cs | 43 +-- 16 files changed, 365 insertions(+), 338 deletions(-) create mode 100644 src/OpenFeature/Error/ProviderFatalException.cs diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 6f13cac2..5440151f 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OpenFeature.Constant; +using OpenFeature.Error; using OpenFeature.Model; namespace OpenFeature @@ -37,7 +38,7 @@ static Api() { } private Api() { } /// - /// Sets the feature provider. In order to wait for the provider to be set, and initialization to complete, + /// 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. @@ -45,10 +46,9 @@ private Api() { } public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProviderAsync(featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); } - /// /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and /// initialization to complete, await the returned task. @@ -62,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro throw new ArgumentNullException(nameof(clientName)); } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); } /// @@ -121,7 +121,7 @@ public FeatureProvider GetProvider(string clientName) /// public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null, EvaluationContext? context = null) => - new FeatureClient(name, version, logger, context); + new FeatureClient(() => _repository.GetProvider(name), name, version, logger, context); /// /// Appends list of hooks to global hooks list @@ -258,6 +258,7 @@ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) public void SetLogger(ILogger logger) { this._eventExecutor.SetLogger(logger); + this._repository.SetLogger(logger); } internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler) @@ -265,5 +266,38 @@ internal void AddClientHandler(string client, ProviderEventTypes eventType, Even 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 + { + 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); + } + + /// + /// 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 + { + 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); + } } } diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs index 232a57cb..4660e41a 100644 --- a/src/OpenFeature/Constant/ErrorType.cs +++ b/src/OpenFeature/Constant/ErrorType.cs @@ -47,5 +47,10 @@ public enum ErrorType /// 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/ProviderStatus.cs b/src/OpenFeature/Constant/ProviderStatus.cs index e56c6c95..16dbd024 100644 --- a/src/OpenFeature/Constant/ProviderStatus.cs +++ b/src/OpenFeature/Constant/ProviderStatus.cs @@ -26,6 +26,11 @@ public enum ProviderStatus /// /// The provider is in an error state and unable to evaluate flags. /// - [Description("ERROR")] Error + [Description("ERROR")] Error, + + /// + /// The provider has entered an irrecoverable error state. + /// + [Description("FATAL")] Fatal, } } diff --git a/src/OpenFeature/Error/ProviderFatalException.cs b/src/OpenFeature/Error/ProviderFatalException.cs new file mode 100644 index 00000000..fae8712a --- /dev/null +++ b/src/OpenFeature/Error/ProviderFatalException.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// the + /// An exception that signals the provider has entered an irrecoverable error state. + /// + [ExcludeFromCodeCoverage] + public class ProviderFatalException : FeatureProviderException + { + /// + /// 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 ca509692..b66201d7 100644 --- a/src/OpenFeature/Error/ProviderNotReadyException.cs +++ b/src/OpenFeature/Error/ProviderNotReadyException.cs @@ -5,7 +5,7 @@ namespace OpenFeature.Error { /// - /// Provider has yet been initialized when evaluating a flag. + /// Provider has not yet been initialized when evaluating a flag. /// [ExcludeFromCodeCoverage] public class ProviderNotReadyException : FeatureProviderException diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 886a47b6..5dfd7dbe 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -184,7 +184,7 @@ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes ev { return; } - var status = provider.GetStatus(); + var status = provider.Status; var message = ""; if (status == ProviderStatus.Ready && eventType == ProviderEventTypes.ProviderReady) @@ -234,6 +234,7 @@ private async void ProcessFeatureProviderEventsAsync(object? providerRef) switch (item) { case ProviderEventPayload eventPayload: + this.UpdateProviderStatus(typedProviderRef, eventPayload); await this.EventChannel.Writer.WriteAsync(new Event { Provider = typedProviderRef, EventPayload = eventPayload }).ConfigureAwait(false); break; } @@ -307,6 +308,24 @@ private async void ProcessEventAsync() } } + // map events to provider status as per spec: https://openfeature.dev/specification/sections/events/#requirement-535 + private void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload eventPayload) + { + 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; + default: break; + } + } + private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) { try diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 62976f53..32635d95 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,10 +1,12 @@ using System.Collections.Immutable; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // required to allow NSubstitute mocking of internal methods namespace OpenFeature { /// @@ -94,22 +96,17 @@ public abstract Task> ResolveStructureValueAsync(string EvaluationContext? context = null, CancellationToken cancellationToken = default); /// - /// Get the status of the provider. + /// Internally-managed provider status. + /// The SDK uses this field to track the status of the provider. + /// Not visible outside OpenFeature assembly /// - /// The current - /// - /// If a provider does not override this method, then its status will be assumed to be - /// . If a provider implements this method, and supports initialization, - /// then it should start in the status . If the status is - /// , then the Api will call the when the - /// provider is set. - /// - public virtual ProviderStatus GetStatus() => ProviderStatus.Ready; + 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. /// /// /// @@ -117,12 +114,7 @@ public abstract Task> ResolveStructureValueAsync(string /// A task that completes when the initialization process is complete. /// /// - /// A provider which supports initialization should override this method as well as - /// . - /// - /// - /// The provider should return or from - /// the method after initialization is complete. + /// Providers not implementing this method will be considered ready immediately. /// /// public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index 4a09c5e8..f39b7f52 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using OpenFeature.Constant; using OpenFeature.Model; namespace OpenFeature @@ -53,6 +54,12 @@ public interface IFeatureClient : IEventBus /// Client metadata ClientMetadata GetMetadata(); + /// + /// Returns the current status of the associated provider. + /// + /// + ProviderStatus ProviderStatus { get; } + /// /// Resolves a boolean feature flag /// diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index 5c48fc19..bdae057e 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -28,6 +28,11 @@ public class ProviderEventPayload /// public string? Message { get; set; } + /// + /// Optional error associated with the event. + /// + public ErrorType? ErrorType { get; set; } + /// /// A List of flags that have been changed. /// diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 674b78a7..767e8b11 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -21,6 +21,7 @@ public sealed partial class FeatureClient : IFeatureClient private readonly ClientMetadata _metadata; private readonly ConcurrentStack _hooks = new ConcurrentStack(); private readonly ILogger _logger; + private readonly Func _providerAccessor; private EvaluationContext _evaluationContext; private readonly object _evaluationContextLock = new object(); @@ -48,6 +49,9 @@ public sealed partial class FeatureClient : IFeatureClient return (method(provider), provider); } + /// + public ProviderStatus ProviderStatus => this._providerAccessor.Invoke().Status; + /// public EvaluationContext GetContext() { @@ -69,16 +73,18 @@ public void SetContext(EvaluationContext? context) /// /// Initializes a new instance of the class. /// + /// Function to retrieve current provider /// Name of client /// Version of client /// Logger used by client /// Context given to this client /// Throws if any of the required parameters are null - public FeatureClient(string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) + internal FeatureClient(Func providerAccessor, string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) { this._metadata = new ClientMetadata(name, version); this._logger = logger ?? NullLogger.Instance; this._evaluationContext = context ?? EvaluationContext.Empty; + this._providerAccessor = providerAccessor; } /// @@ -246,6 +252,16 @@ private async Task> EvaluateFlagAsync( { var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false); + // short circuit evaluation entirely if provider is in a bad state + if (provider.Status == ProviderStatus.NotReady) + { + throw new ProviderNotReadyException("Provider has not yet completed initialization."); + } + else if (provider.Status == ProviderStatus.Fatal) + { + throw new ProviderFatalException("Provider is in an irrecoverable error state."); + } + evaluation = (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false)) .ToFlagEvaluationDetails(); diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 7934da1c..1656fdd3 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using OpenFeature.Constant; using OpenFeature.Model; @@ -14,6 +16,8 @@ namespace OpenFeature /// internal sealed class ProviderRepository : IAsyncDisposable { + private ILogger _logger; + private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); private readonly ConcurrentDictionary _featureProviders = @@ -31,6 +35,11 @@ internal sealed class ProviderRepository : IAsyncDisposable /// of that provider under different names.. private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); + public ProviderRepository() + { + this._logger = NullLogger.Instance; + } + public async ValueTask DisposeAsync() { using (this._providersLock) @@ -39,36 +48,25 @@ public async ValueTask DisposeAsync() } } + internal void SetLogger(ILogger logger) => this._logger = logger; + /// /// Set the default provider /// /// the provider to set as the default, passing null has no effect /// the context to initialize the provider with - /// - /// - /// Called after the provider is set, but before any actions are taken on it. - /// - /// This can be used for tasks such as registering event handlers. It should be noted that this can be called - /// several times for a single provider. For instance registering a provider with multiple names or as the - /// default and named provider. - /// - /// - /// - /// + /// /// called after the provider has initialized successfully, only called if the provider needed initialization /// - /// + /// /// called if an error happens during the initialization of the provider, only called if the provider needed /// initialization /// - /// called after a provider is shutdown, can be used to remove event handlers public async Task SetProviderAsync( FeatureProvider? featureProvider, EvaluationContext context, - Action? afterSet = null, - Action? afterInitialization = null, - Action? afterError = null, - Action? afterShutdown = null) + Func? afterInitSuccess = null, + Func? afterInitError = null) { // Cannot unset the feature provider. if (featureProvider == null) @@ -88,42 +86,45 @@ public async Task SetProviderAsync( var oldProvider = this._defaultProvider; this._defaultProvider = featureProvider; - afterSet?.Invoke(featureProvider); // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. -#pragma warning disable CS4014 - this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); -#pragma warning restore CS4014 + _ = this.ShutdownIfUnusedAsync(oldProvider); } finally { this._providersLock.ExitWriteLock(); } - await InitProviderAsync(this._defaultProvider, context, afterInitialization, afterError) + await InitProviderAsync(this._defaultProvider, context, afterInitSuccess, afterInitError) .ConfigureAwait(false); } private static async Task InitProviderAsync( FeatureProvider? newProvider, EvaluationContext context, - Action? afterInitialization, - Action? afterError) + Func? afterInitialization, + Func? afterError) { if (newProvider == null) { return; } - if (newProvider.GetStatus() == ProviderStatus.NotReady) + if (newProvider.Status == ProviderStatus.NotReady) { try { await newProvider.InitializeAsync(context).ConfigureAwait(false); - afterInitialization?.Invoke(newProvider); + if (afterInitialization != null) + { + await afterInitialization.Invoke(newProvider).ConfigureAwait(false); + } } catch (Exception ex) { - afterError?.Invoke(newProvider, ex); + if (afterError != null) + { + await afterError.Invoke(newProvider, ex).ConfigureAwait(false); + } } } } @@ -134,32 +135,19 @@ private static async Task InitProviderAsync( /// the name to associate with the provider /// the provider to set as the default, passing null has no effect /// the context to initialize the provider with - /// - /// - /// Called after the provider is set, but before any actions are taken on it. - /// - /// This can be used for tasks such as registering event handlers. It should be noted that this can be called - /// several times for a single provider. For instance registering a provider with multiple names or as the - /// default and named provider. - /// - /// - /// - /// + /// /// called after the provider has initialized successfully, only called if the provider needed initialization /// - /// + /// /// called if an error happens during the initialization of the provider, only called if the provider needed /// initialization /// - /// called after a provider is shutdown, can be used to remove event handlers /// The to cancel any async side effects. - public async Task SetProviderAsync(string clientName, + public async Task SetProviderAsync(string? clientName, FeatureProvider? featureProvider, EvaluationContext context, - Action? afterSet = null, - Action? afterInitialization = null, - Action? afterError = null, - Action? afterShutdown = null, + Func? afterInitSuccess = null, + Func? afterInitError = null, CancellationToken cancellationToken = default) { // Cannot set a provider for a null clientName. @@ -177,7 +165,6 @@ public async Task SetProviderAsync(string clientName, { this._featureProviders.AddOrUpdate(clientName, featureProvider, (key, current) => featureProvider); - afterSet?.Invoke(featureProvider); } else { @@ -188,25 +175,21 @@ public async Task SetProviderAsync(string clientName, // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. -#pragma warning disable CS4014 - this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); -#pragma warning restore CS4014 + _ = this.ShutdownIfUnusedAsync(oldProvider); } finally { this._providersLock.ExitWriteLock(); } - await InitProviderAsync(featureProvider, context, afterInitialization, afterError).ConfigureAwait(false); + await InitProviderAsync(featureProvider, context, afterInitSuccess, afterInitError).ConfigureAwait(false); } /// /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. /// private async Task ShutdownIfUnusedAsync( - FeatureProvider? targetProvider, - Action? afterShutdown, - Action? afterError) + FeatureProvider? targetProvider) { if (ReferenceEquals(this._defaultProvider, targetProvider)) { @@ -218,7 +201,7 @@ private async Task ShutdownIfUnusedAsync( return; } - await SafeShutdownProviderAsync(targetProvider, afterShutdown, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } /// @@ -230,9 +213,7 @@ private async Task ShutdownIfUnusedAsync( /// it would not be meaningful to emit an error. /// /// - private static async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider, - Action? afterShutdown, - Action? afterError) + private async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider) { if (targetProvider == null) { @@ -242,11 +223,10 @@ private static async Task SafeShutdownProviderAsync(FeatureProvider? targetProvi try { await targetProvider.ShutdownAsync().ConfigureAwait(false); - afterShutdown?.Invoke(targetProvider); } catch (Exception ex) { - afterError?.Invoke(targetProvider, ex); + this._logger.LogError(ex, $"Error shutting down provider: {targetProvider.GetMetadata().Name}"); } } @@ -307,7 +287,7 @@ public async Task ShutdownAsync(Action? afterError = foreach (var targetProvider in providers) { // We don't need to take any actions after shutdown. - await SafeShutdownProviderAsync(targetProvider, null, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index e7c76d75..925de66a 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -185,6 +185,92 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedLogger.Received(1).IsEnabled(LogLevel.Error); } + [Fact] + [Specification("1.7.3", "The client's provider status accessor MUST indicate READY if the initialize function of the associated provider terminates normally.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Ready_If_Init_Succeeds() + { + var name = "1.7.3"; + // provider which succeeds initialization + var provider = new TestProvider(); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be READY + Assert.Equal(ProviderStatus.Ready, client.ProviderStatus); + } + + [Fact] + [Specification("1.7.4", "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Error_If_Init_Fails() + { + var name = "1.7.4"; + // provider which fails initialization + var provider = new TestProvider("some-name", new GeneralException("fake")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be ERROR + Assert.Equal(ProviderStatus.Error, client.ProviderStatus); + } + + [Fact] + [Specification("1.7.5", "The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider terminates abnormally and indicates error code PROVIDER_FATAL.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Fatal_If_Init_Fatal() + { + var name = "1.7.5"; + // provider which fails initialization fatally + var provider = new TestProvider(name, new ProviderFatalException("fatal")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be FATAL + Assert.Equal(ProviderStatus.Fatal, client.ProviderStatus); + } + + [Fact] + [Specification("1.7.6", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] + public async Task Must_Short_Circuit_Not_Ready() + { + var name = "1.7.6"; + var defaultStr = "123-default"; + + // provider which is never ready (ready after maxValue) + var provider = new TestProvider(name, null, int.MaxValue); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + _ = Api.Instance.SetProviderAsync(name, provider); + + var details = await client.GetStringDetailsAsync("some-flag", defaultStr); + Assert.Equal(defaultStr, details.Value); + Assert.Equal(ErrorType.ProviderNotReady, details.ErrorType); + Assert.Equal(Reason.Error, details.Reason); + } + + [Fact] + [Specification("1.7.7", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] + public async Task Must_Short_Circuit_Fatal() + { + var name = "1.7.6"; + var defaultStr = "456-default"; + + // provider which immediately fails fatally + var provider = new TestProvider(name, new ProviderFatalException("fake")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + _ = Api.Instance.SetProviderAsync(name, provider); + + var details = await client.GetStringDetailsAsync("some-flag", defaultStr); + Assert.Equal(defaultStr, details.Value); + Assert.Equal(ErrorType.ProviderFatal, details.ErrorType); + Assert.Equal(Reason.Error, details.Reason); + } + [Fact] public async Task Should_Resolve_BooleanValue() { @@ -358,15 +444,15 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() var cts = new CancellationTokenSource(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => { var token = args.ArgAt(3); - while (!token.IsCancellationRequested) + while (!token.IsCancellationRequested) { await Task.Delay(10); // artificially delay until cancelled } - return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); + return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); }); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 384928d6..a7bcd2e7 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -147,9 +147,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ var eventHandler = Substitute.For(); var testProvider = new TestProvider(); -#pragma warning disable CS0618// Type or member is obsolete await Api.Instance.SetProviderAsync(testProvider); -#pragma warning restore CS0618// Type or member is obsolete Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -175,7 +173,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_ var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SetStatus(ProviderStatus.Error); + testProvider.Status = ProviderStatus.Error; Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); @@ -200,7 +198,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_ var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SetStatus(ProviderStatus.Stale); + testProvider.Status = ProviderStatus.Stale; Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); @@ -476,7 +474,11 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); // wait for the first event to be received - await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); + await Utils.AssertUntilAsync( + _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + + myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); // send another event from the provider - this one should not be received await testProvider.SendEventAsync(ProviderEventTypes.ProviderReady); @@ -501,5 +503,18 @@ public void RegisterClientFeatureProvider_WhenCalledWithNullProvider_DoesNotThro // Assert Assert.Null(exception); } + + [Theory] + [InlineData(ProviderEventTypes.ProviderError, ProviderStatus.Error)] + [InlineData(ProviderEventTypes.ProviderReady, ProviderStatus.Ready)] + [InlineData(ProviderEventTypes.ProviderStale, ProviderStatus.Stale)] + [Specification("5.3.5", "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")] + public async Task Provider_Events_Should_Update_ProviderStatus(ProviderEventTypes type, ProviderStatus status) + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync("5.3.5", provider); + _ = provider.SendEventAsync(type); + await Utils.AssertUntilAsync(_ => Assert.True(provider.Status == status)); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 673c183d..1df3c976 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -28,13 +28,13 @@ public void OpenFeature_Should_Be_Singleton() public async Task OpenFeature_Should_Initialize_Provider() { var providerMockDefault = Substitute.For(); - providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); + providerMockDefault.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerMockDefault); await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerMockNamed = Substitute.For(); - providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); + providerMockNamed.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("the-name", providerMockNamed); await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()); @@ -46,26 +46,26 @@ public async Task OpenFeature_Should_Initialize_Provider() public async Task OpenFeature_Should_Shutdown_Unused_Provider() { var providerA = Substitute.For(); - providerA.GetStatus().Returns(ProviderStatus.NotReady); + providerA.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerA); await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerB = Substitute.For(); - providerB.GetStatus().Returns(ProviderStatus.NotReady); + providerB.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerB); await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()); await providerA.Received(1).ShutdownAsync(); var providerC = Substitute.For(); - providerC.GetStatus().Returns(ProviderStatus.NotReady); + providerC.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("named", providerC); await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerD = Substitute.For(); - providerD.GetStatus().Returns(ProviderStatus.NotReady); + providerD.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("named", providerD); await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()); @@ -77,10 +77,10 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() public async Task OpenFeature_Should_Support_Shutdown() { var providerA = Substitute.For(); - providerA.GetStatus().Returns(ProviderStatus.NotReady); + providerA.Status.Returns(ProviderStatus.NotReady); var providerB = Substitute.For(); - providerB.GetStatus().Returns(ProviderStatus.NotReady); + providerB.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerA); await Api.Instance.SetProviderAsync("named", providerB); diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index ccec89bd..e88de6e9 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using NSubstitute; -using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; using OpenFeature.Model; using Xunit; @@ -25,28 +24,12 @@ public async Task Default_Provider_Is_Set_Without_Await() Assert.Equal(provider, repository.GetProvider()); } - [Fact] - public async Task AfterSet_Is_Invoked_For_Setting_Default_Provider() - { - var repository = new ProviderRepository(); - var provider = new NoOpFeatureProvider(); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProviderAsync(provider, context, afterSet: (theProvider) => - { - callCount++; - Assert.Equal(provider, theProvider); - }); - Assert.Equal(1, callCount); - } - [Fact] public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Default_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(providerMock, context); providerMock.Received(1).InitializeAsync(context); @@ -58,13 +41,14 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Default_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProviderAsync(providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync(providerMock, context, afterInitSuccess: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; + return Task.CompletedTask; }); Assert.Equal(1, callCount); } @@ -74,16 +58,17 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provide { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; Exception? receivedError = null; - await repository.SetProviderAsync(providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync(providerMock, context, afterInitError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; + return Task.CompletedTask; }); Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); @@ -93,11 +78,11 @@ await repository.SetProviderAsync(providerMock, context, afterError: (theProvide [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + internal async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(providerMock, context); providerMock.DidNotReceive().InitializeAsync(context); @@ -107,14 +92,18 @@ public async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus sta [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + internal async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProviderAsync(providerMock, context, afterInitialization: provider => { callCount++; }); + await repository.SetProviderAsync(providerMock, context, afterInitSuccess: provider => + { + callCount++; + return Task.CompletedTask; + }); Assert.Equal(0, callCount); } @@ -123,10 +112,10 @@ public async Task Replaced_Default_Provider_Is_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(provider1, context); @@ -135,52 +124,6 @@ public async Task Replaced_Default_Provider_Is_Shutdown() provider2.DidNotReceive().ShutdownAsync(); } - [Fact] - public async Task AfterShutdown_Is_Called_For_Shutdown_Provider() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider1, context); - var callCount = 0; - await repository.SetProviderAsync(provider2, context, afterShutdown: provider => - { - Assert.Equal(provider, provider1); - callCount++; - }); - Assert.Equal(1, callCount); - } - - [Fact] - public async Task AfterError_Is_Called_For_Shutdown_That_Throws() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider1, context); - var callCount = 0; - Exception? errorThrown = null; - await repository.SetProviderAsync(provider2, context, afterError: (provider, ex) => - { - Assert.Equal(provider, provider1); - errorThrown = ex; - callCount++; - }); - Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); - } - [Fact] public async Task Named_Provider_Provider_Is_Set_Without_Await() { @@ -192,28 +135,12 @@ public async Task Named_Provider_Provider_Is_Set_Without_Await() Assert.Equal(provider, repository.GetProvider("the-name")); } - [Fact] - public async Task AfterSet_Is_Invoked_For_Setting_Named_Provider() - { - var repository = new ProviderRepository(); - var provider = new NoOpFeatureProvider(); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => - { - callCount++; - Assert.Equal(provider, theProvider); - }); - Assert.Equal(1, callCount); - } - [Fact] public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Named_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync("the-name", providerMock, context); providerMock.Received(1).InitializeAsync(context); @@ -225,13 +152,14 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Named_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProviderAsync("the-name", providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync("the-name", providerMock, context, afterInitSuccess: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; + return Task.CompletedTask; }); Assert.Equal(1, callCount); } @@ -241,16 +169,17 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider( { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; Exception? receivedError = null; - await repository.SetProviderAsync("the-provider", providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync("the-provider", providerMock, context, afterInitError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; + return Task.CompletedTask; }); Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); @@ -260,11 +189,11 @@ await repository.SetProviderAsync("the-provider", providerMock, context, afterEr [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + internal async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync("the-name", providerMock, context); providerMock.DidNotReceive().InitializeAsync(context); @@ -274,15 +203,19 @@ public async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStat [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + internal async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; await repository.SetProviderAsync("the-name", providerMock, context, - afterInitialization: provider => { callCount++; }); + afterInitSuccess: provider => + { + callCount++; + return Task.CompletedTask; + }); Assert.Equal(0, callCount); } @@ -291,10 +224,10 @@ public async Task Replaced_Named_Provider_Is_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync("the-name", provider1, context); @@ -303,61 +236,15 @@ public async Task Replaced_Named_Provider_Is_Shutdown() provider2.DidNotReceive().ShutdownAsync(); } - [Fact] - public async Task AfterShutdown_Is_Called_For_Shutdown_Named_Provider() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync("the-provider", provider1, context); - var callCount = 0; - await repository.SetProviderAsync("the-provider", provider2, context, afterShutdown: provider => - { - Assert.Equal(provider, provider1); - callCount++; - }); - Assert.Equal(1, callCount); - } - - [Fact] - public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync("the-name", provider1, context); - var callCount = 0; - Exception? errorThrown = null; - await repository.SetProviderAsync("the-name", provider2, context, afterError: (provider, ex) => - { - Assert.Equal(provider, provider1); - errorThrown = ex; - callCount++; - }); - Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); - } - [Fact] public async Task In_Use_Provider_Named_And_Default_Is_Not_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -374,10 +261,10 @@ public async Task In_Use_Provider_Two_Named_Is_Not_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -394,10 +281,10 @@ public async Task When_All_Instances_Are_Removed_Shutdown_Is_Called() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -415,10 +302,10 @@ public async Task Can_Get_Providers_By_Name() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -434,10 +321,10 @@ public async Task Replaced_Named_Provider_Gets_Latest_Set() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -452,13 +339,13 @@ public async Task Can_Shutdown_All_Providers() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var provider3 = Substitute.For(); - provider3.GetStatus().Returns(ProviderStatus.NotReady); + provider3.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -475,62 +362,12 @@ public async Task Can_Shutdown_All_Providers() provider3.Received(1).ShutdownAsync(); } - [Fact] - public async Task Errors_During_Shutdown_Propagate() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 1")); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - provider2.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 2")); - - var provider3 = Substitute.For(); - provider3.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - - await repository.SetProviderAsync(provider1, context); - await repository.SetProviderAsync("provider1", provider1, context); - await repository.SetProviderAsync("provider2", provider2, context); - await repository.SetProviderAsync("provider2a", provider2, context); - await repository.SetProviderAsync("provider3", provider3, context); - - var callCountShutdown1 = 0; - var callCountShutdown2 = 0; - var totalCallCount = 0; - await repository.ShutdownAsync(afterError: (provider, exception) => - { - totalCallCount++; - if (provider == provider1) - { - callCountShutdown1++; - Assert.Equal("SHUTDOWN ERROR 1", exception.Message); - } - - if (provider == provider2) - { - callCountShutdown2++; - Assert.Equal("SHUTDOWN ERROR 2", exception.Message); - } - }); - Assert.Equal(2, totalCallCount); - Assert.Equal(1, callCountShutdown1); - Assert.Equal(1, callCountShutdown2); - - provider1.Received(1).ShutdownAsync(); - provider2.Received(1).ShutdownAsync(); - provider3.Received(1).ShutdownAsync(); - } - [Fact] public async Task Setting_Same_Default_Provider_Has_No_Effect() { var repository = new ProviderRepository(); var provider = Substitute.For(); - provider.GetStatus().Returns(ProviderStatus.NotReady); + provider.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(provider, context); await repository.SetProviderAsync(provider, context); @@ -545,7 +382,7 @@ public async Task Setting_Null_Default_Provider_Has_No_Effect() { var repository = new ProviderRepository(); var provider = Substitute.For(); - provider.GetStatus().Returns(ProviderStatus.NotReady); + provider.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(provider, context); await repository.SetProviderAsync(null, context); @@ -561,10 +398,10 @@ public async Task Setting_Null_Named_Provider_Removes_It() var repository = new ProviderRepository(); var namedProvider = Substitute.For(); - namedProvider.GetStatus().Returns(ProviderStatus.NotReady); + namedProvider.Status.Returns(ProviderStatus.NotReady); var defaultProvider = Substitute.For(); - defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); + defaultProvider.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(defaultProvider, context); diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index c949b373..a4fe51a4 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -40,24 +40,30 @@ public class TestProvider : FeatureProvider public static string DefaultName = "test-provider"; - public string Name { get; set; } - - private ProviderStatus _status; + public string? Name { get; set; } public void AddHook(Hook hook) => this._hooks.Add(hook); public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList(); + private Exception? initException = null; + private int initDelay = 0; public TestProvider() { - this._status = ProviderStatus.NotReady; this.Name = DefaultName; } - public TestProvider(string name) + /// + /// A provider used for testing. + /// + /// the name of the provider. + /// Optional exception to throw during init. + /// + public TestProvider(string? name, Exception? initException = null, int initDelay = 0) { - this._status = ProviderStatus.NotReady; - this.Name = name; + this.Name = string.IsNullOrEmpty(name) ? DefaultName : name; + this.initException = initException; + this.initDelay = initDelay; } public override Metadata GetMetadata() @@ -95,26 +101,23 @@ public override Task> ResolveStructureValueAsync(string return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override ProviderStatus GetStatus() - { - return this._status; - } - - public void SetStatus(ProviderStatus status) + public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { - this._status = status; + await Task.Delay(initDelay).ConfigureAwait(false); + if (initException != null) + { + throw initException; + } } - public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) + internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) { - this._status = ProviderStatus.Ready; - await this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }, cancellationToken).ConfigureAwait(false); - await base.InitializeAsync(context, cancellationToken).ConfigureAwait(false); + return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name, }, cancellationToken); } - internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) + internal ValueTask SendEventAsync(ProviderEventPayload payload, CancellationToken cancellationToken = default) { - return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name }, cancellationToken); + return this.EventChannel.Writer.WriteAsync(payload, cancellationToken); } } } From 44cf586f96607716fb8b4464d81edfd6074f7376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 23 Jul 2024 19:19:10 +0100 Subject: [PATCH 185/316] chore: cleanup code (#277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This PR fixes some of the warnings and typos that I recently found. More interestingly, it addresses these issues: - Missing the `.this` - Usage of `ILogger` vs `Source generator log` - `const` vs `static` - Fix nullability for some methods and properties. And a few more changes. ### Follow-up Tasks We need to do more cleanup tasks. ### How to test All of these changes are recommended by the IDE and "tested" by the compiler when it executes. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Api.cs | 19 ++++--- src/OpenFeature/Constant/Reason.cs | 16 +++--- .../Error/ProviderFatalException.cs | 2 +- src/OpenFeature/EventExecutor.cs | 3 +- src/OpenFeature/FeatureProvider.cs | 8 +-- .../Model/EvaluationContextBuilder.cs | 4 +- .../Model/FlagEvaluationOptions.cs | 4 +- src/OpenFeature/Model/ImmutableMetadata.cs | 1 - src/OpenFeature/Model/Value.cs | 18 +++---- src/OpenFeature/OpenFeatureClient.cs | 15 +++--- src/OpenFeature/ProviderRepository.cs | 26 +++++----- src/OpenFeature/Providers/Memory/Flag.cs | 31 +++++------ .../Providers/Memory/InMemoryProvider.cs | 30 +++++------ .../OpenFeatureClientBenchmarks.cs | 52 +++++++++---------- .../Steps/EvaluationStepDefinitions.cs | 18 +++---- test/OpenFeature.Tests/OpenFeatureTests.cs | 10 ++-- test/OpenFeature.Tests/TestImplementations.cs | 6 +-- 17 files changed, 125 insertions(+), 138 deletions(-) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 5440151f..3fa38916 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -32,7 +32,7 @@ public sealed class Api : IEventBus public static Api Instance { get; } = new Api(); // Explicit static constructor to tell C# compiler - // not to mark type as beforefieldinit + // not to mark type as beforeFieldInit // IE Lazy way of ensuring this is thread safe without using locks static Api() { } private Api() { } @@ -46,7 +46,7 @@ private Api() { } public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProviderAsync(featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); } /// @@ -62,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro throw new ArgumentNullException(nameof(clientName)); } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); + await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); } /// @@ -101,7 +101,7 @@ public FeatureProvider GetProvider(string clientName) /// /// /// - public Metadata GetProviderMetadata() => this.GetProvider().GetMetadata(); + public Metadata? GetProviderMetadata() => this.GetProvider().GetMetadata(); /// /// Gets providers metadata assigned to the given clientName. If the clientName has no provider @@ -109,7 +109,7 @@ public FeatureProvider GetProvider(string clientName) /// /// Name of client /// Metadata assigned to provider - public Metadata GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); + public Metadata? GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); /// /// Create a new instance of using the current provider @@ -121,7 +121,7 @@ public FeatureProvider GetProvider(string clientName) /// public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null, EvaluationContext? context = null) => - new FeatureClient(() => _repository.GetProvider(name), name, version, logger, context); + new FeatureClient(() => this._repository.GetProvider(name), name, version, logger, context); /// /// Appends list of hooks to global hooks list @@ -277,7 +277,7 @@ private async Task AfterInitialization(FeatureProvider provider) { Type = ProviderEventTypes.ProviderReady, Message = "Provider initialization complete", - ProviderName = provider.GetMetadata().Name, + ProviderName = provider.GetMetadata()?.Name, }; await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false); @@ -286,10 +286,9 @@ private async Task AfterInitialization(FeatureProvider provider) /// /// Update the provider state to ERROR and emit an ERROR after failed init. /// - private async Task AfterError(FeatureProvider provider, Exception ex) - + private async Task AfterError(FeatureProvider provider, Exception? ex) { - provider.Status = typeof(ProviderFatalException) == ex.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error; + provider.Status = typeof(ProviderFatalException) == ex?.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error; var eventPayload = new ProviderEventPayload { Type = ProviderEventTypes.ProviderError, diff --git a/src/OpenFeature/Constant/Reason.cs b/src/OpenFeature/Constant/Reason.cs index a60ce78a..eac06c1e 100644 --- a/src/OpenFeature/Constant/Reason.cs +++ b/src/OpenFeature/Constant/Reason.cs @@ -9,42 +9,42 @@ public static class Reason /// /// Use when the flag is matched based on the evaluation context user data /// - public static string TargetingMatch = "TARGETING_MATCH"; + public const string TargetingMatch = "TARGETING_MATCH"; /// /// Use when the flag is matched based on a split rule in the feature flag provider /// - public static string Split = "SPLIT"; + public const string Split = "SPLIT"; /// /// Use when the flag is disabled in the feature flag provider /// - public static string Disabled = "DISABLED"; + public const string Disabled = "DISABLED"; /// /// Default reason when evaluating flag /// - public static string Default = "DEFAULT"; + public const string Default = "DEFAULT"; /// /// The resolved value is static (no dynamic evaluation) /// - public static string Static = "STATIC"; + public const string Static = "STATIC"; /// /// The resolved value was retrieved from cache /// - public static string Cached = "CACHED"; + 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 static string Unknown = "UNKNOWN"; + public const string Unknown = "UNKNOWN"; /// /// Use this flag when abnormal execution is encountered. /// - public static string Error = "ERROR"; + public const string Error = "ERROR"; } } diff --git a/src/OpenFeature/Error/ProviderFatalException.cs b/src/OpenFeature/Error/ProviderFatalException.cs index fae8712a..894a583d 100644 --- a/src/OpenFeature/Error/ProviderFatalException.cs +++ b/src/OpenFeature/Error/ProviderFatalException.cs @@ -4,7 +4,7 @@ namespace OpenFeature.Error { - /// the + /// /// An exception that signals the provider has entered an irrecoverable error state. /// [ExcludeFromCodeCoverage] diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 5dfd7dbe..ad53a949 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -206,7 +206,7 @@ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes ev { handler.Invoke(new ProviderEventPayload { - ProviderName = provider.GetMetadata().Name, + ProviderName = provider.GetMetadata()?.Name, Type = eventType, Message = message }); @@ -322,6 +322,7 @@ private void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload case ProviderEventTypes.ProviderError: provider.Status = eventPayload.ErrorType == ErrorType.ProviderFatal ? ProviderStatus.Fatal : ProviderStatus.Error; break; + case ProviderEventTypes.ProviderConfigurationChanged: default: break; } } diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 32635d95..c4ce8783 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -11,14 +11,14 @@ namespace OpenFeature { /// /// The provider interface describes the abstraction layer for a feature flag provider. - /// A provider acts as the translates layer between the generic feature flag structure to a target feature flag system. + /// 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 { /// - /// Gets a immutable list of hooks that belong to the provider. - /// By default return a empty list + /// 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 @@ -38,7 +38,7 @@ public abstract class FeatureProvider /// Metadata describing the provider. /// /// - public abstract Metadata GetMetadata(); + public abstract Metadata? GetMetadata(); /// /// Resolves a boolean feature flag diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs index 1afb02fc..c672c401 100644 --- a/src/OpenFeature/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -140,9 +140,9 @@ public EvaluationContextBuilder Merge(EvaluationContext context) { string? newTargetingKey = ""; - if (!string.IsNullOrWhiteSpace(TargetingKey)) + if (!string.IsNullOrWhiteSpace(this.TargetingKey)) { - newTargetingKey = TargetingKey; + newTargetingKey = this.TargetingKey; } if (!string.IsNullOrWhiteSpace(context.TargetingKey)) diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs index 7bde600c..8bba0aef 100644 --- a/src/OpenFeature/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs @@ -10,12 +10,12 @@ namespace OpenFeature.Model public sealed class FlagEvaluationOptions { /// - /// A immutable list of + /// An immutable list of /// public IImmutableList Hooks { get; } /// - /// A immutable dictionary of hook hints + /// An immutable dictionary of hook hints /// public IImmutableDictionary HookHints { get; } diff --git a/src/OpenFeature/Model/ImmutableMetadata.cs b/src/OpenFeature/Model/ImmutableMetadata.cs index 40d452d0..1f2c6f8a 100644 --- a/src/OpenFeature/Model/ImmutableMetadata.cs +++ b/src/OpenFeature/Model/ImmutableMetadata.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; -#nullable enable namespace OpenFeature.Model; /// diff --git a/src/OpenFeature/Model/Value.cs b/src/OpenFeature/Model/Value.cs index 5af3b8b3..88fb0734 100644 --- a/src/OpenFeature/Model/Value.cs +++ b/src/OpenFeature/Model/Value.cs @@ -139,49 +139,49 @@ public Value(Object value) public object? AsObject => this._innerValue; /// - /// Returns the underlying int value - /// Value will be null if it isn't a integer + /// Returns the underlying int value. + /// Value will be null if it isn't an integer /// /// Value as int - public int? AsInteger => this.IsNumber ? (int?)Convert.ToInt32((double?)this._innerValue) : null; + public int? AsInteger => this.IsNumber ? Convert.ToInt32((double?)this._innerValue) : null; /// - /// Returns the underlying bool value + /// Returns the underlying bool value. /// Value will be null if it isn't a bool /// /// Value as bool public bool? AsBoolean => this.IsBoolean ? (bool?)this._innerValue : null; /// - /// Returns the underlying double value + /// Returns the underlying double value. /// Value will be null if it isn't a double /// /// Value as int public double? AsDouble => this.IsNumber ? (double?)this._innerValue : null; /// - /// Returns the underlying string value + /// Returns the underlying string value. /// Value will be null if it isn't a string /// /// Value as string public string? AsString => this.IsString ? (string?)this._innerValue : null; /// - /// Returns the underlying Structure value + /// Returns the underlying Structure value. /// Value will be null if it isn't a Structure /// /// Value as Structure public Structure? AsStructure => this.IsStructure ? (Structure?)this._innerValue : null; /// - /// Returns the underlying List value + /// Returns the underlying List value. /// Value will be null if it isn't a List /// /// Value as List public IImmutableList? AsList => this.IsList ? (IImmutableList?)this._innerValue : null; /// - /// Returns the underlying DateTime value + /// Returns the underlying DateTime value. /// Value will be null if it isn't a DateTime /// /// Value as DateTime diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 767e8b11..08e29533 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -212,11 +212,8 @@ private async Task> EvaluateFlagAsync( var resolveValueDelegate = providerInfo.Item1; var provider = providerInfo.Item2; - // New up a evaluation context if one was not provided. - if (context == null) - { - context = EvaluationContext.Empty; - } + // New up an evaluation context if one was not provided. + context ??= EvaluationContext.Empty; // merge api, client, and invocation context. var evaluationContext = Api.Instance.GetContext(); @@ -253,11 +250,11 @@ private async Task> EvaluateFlagAsync( var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false); // short circuit evaluation entirely if provider is in a bad state - if (provider.Status == ProviderStatus.NotReady) + if (provider.Status == ProviderStatus.NotReady) { throw new ProviderNotReadyException("Provider has not yet completed initialization."); - } - else if (provider.Status == ProviderStatus.Fatal) + } + else if (provider.Status == ProviderStatus.Fatal) { throw new ProviderFatalException("Provider is in an irrecoverable error state."); } @@ -349,7 +346,7 @@ private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, HookCo } catch (Exception e) { - this._logger.LogError(e, "Error while executing Finally hook {HookName}", hook.GetType().Name); + this.FinallyHookError(hook.GetType().Name, e); } } } diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 1656fdd3..760503b6 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -14,9 +14,9 @@ namespace OpenFeature /// /// This class manages the collection of providers, both default and named, contained by the API. /// - internal sealed class ProviderRepository : IAsyncDisposable + internal sealed partial class ProviderRepository : IAsyncDisposable { - private ILogger _logger; + private ILogger _logger = NullLogger.Instance; private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); @@ -26,20 +26,15 @@ internal sealed class ProviderRepository : IAsyncDisposable /// The reader/writer locks is not disposed because the singleton instance should never be disposed. /// /// Mutations of the _defaultProvider or _featureProviders are done within this lock even though - /// _featureProvider is a concurrent collection. This is for a couple reasons, the first is that + /// _featureProvider is a concurrent collection. This is for a couple of reasons, the first is that /// a provider should only be shutdown if it is not in use, and it could be in use as either a named or /// default provider. /// - /// The second is that a concurrent collection doesn't provide any ordering so we could check a provider + /// The second is that a concurrent collection doesn't provide any ordering, so we could check a provider /// as it was being added or removed such as two concurrent calls to SetProvider replacing multiple instances - /// of that provider under different names.. + /// of that provider under different names. private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); - public ProviderRepository() - { - this._logger = NullLogger.Instance; - } - public async ValueTask DisposeAsync() { using (this._providersLock) @@ -201,7 +196,7 @@ private async Task ShutdownIfUnusedAsync( return; } - await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); + await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } /// @@ -209,7 +204,7 @@ private async Task ShutdownIfUnusedAsync( /// Shut down the provider and capture any exceptions thrown. /// /// - /// The provider is set either to a name or default before the old provider it shutdown, so + /// The provider is set either to a name or default before the old provider it shut down, so /// it would not be meaningful to emit an error. /// /// @@ -226,7 +221,7 @@ private async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider) } catch (Exception ex) { - this._logger.LogError(ex, $"Error shutting down provider: {targetProvider.GetMetadata().Name}"); + this.ErrorShuttingDownProvider(targetProvider.GetMetadata()?.Name, ex); } } @@ -287,8 +282,11 @@ public async Task ShutdownAsync(Action? afterError = foreach (var targetProvider in providers) { // We don't need to take any actions after shutdown. - await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); + await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } } + + [LoggerMessage(EventId = 105, Level = LogLevel.Error, Message = "Error shutting down provider: {TargetProviderName}`")] + partial void ErrorShuttingDownProvider(string? targetProviderName, Exception exception); } } diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 1a16bfe3..5cee86ea 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -9,19 +9,16 @@ namespace OpenFeature.Providers.Memory /// /// Flag representation for the in-memory provider. /// - public interface Flag - { - - } + public interface Flag; /// /// Flag representation for the in-memory provider. /// public sealed class Flag : Flag { - private Dictionary Variants; - private string DefaultVariant; - private Func? ContextEvaluator; + private readonly Dictionary _variants; + private readonly string _defaultVariant; + private readonly Func? _contextEvaluator; /// /// Flag representation for the in-memory provider. @@ -31,34 +28,34 @@ public sealed class Flag : Flag /// optional context-sensitive evaluation function public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null) { - this.Variants = variants; - this.DefaultVariant = defaultVariant; - this.ContextEvaluator = contextEvaluator; + this._variants = variants; + this._defaultVariant = defaultVariant; + this._contextEvaluator = contextEvaluator; } internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) { - T? value = default; - if (this.ContextEvaluator == null) + T? value; + if (this._contextEvaluator == null) { - if (this.Variants.TryGetValue(this.DefaultVariant, out value)) + if (this._variants.TryGetValue(this._defaultVariant, out value)) { return new ResolutionDetails( flagKey, value, - variant: this.DefaultVariant, + variant: this._defaultVariant, reason: Reason.Static ); } else { - throw new GeneralException($"variant {this.DefaultVariant} not found"); + throw new GeneralException($"variant {this._defaultVariant} not found"); } } else { - var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); - if (!this.Variants.TryGetValue(variant, out value)) + var variant = this._contextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); + if (!this._variants.TryGetValue(variant, out value)) { throw new GeneralException($"variant {variant} not found"); } diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index e56acdb5..771e2210 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -61,7 +61,7 @@ public async Task UpdateFlags(IDictionary? flags = null) var @event = new ProviderEventPayload { Type = ProviderEventTypes.ProviderConfigurationChanged, - ProviderName = _metadata.Name, + ProviderName = this._metadata.Name, FlagsChanged = changed, // emit all Message = "flags changed", }; @@ -71,31 +71,31 @@ public async Task UpdateFlags(IDictionary? flags = null) /// public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext? context) @@ -104,19 +104,15 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati { throw new FlagNotFoundException($"flag {flagKey} not found"); } - else + + // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. + // In a production provider, such behavior is probably not desirable; consider supporting conversion. + if (flag is Flag value) { - // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. - // In a production provider, such behavior is probably not desirable; consider supporting conversion. - if (typeof(Flag).Equals(flag.GetType())) - { - return ((Flag)flag).Evaluate(flagKey, defaultValue, context); - } - else - { - throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}"); - } + return value.Evaluate(flagKey, defaultValue, context); } + + throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}"); } } } diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 7f2e5b30..3796821e 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -30,77 +30,77 @@ public class OpenFeatureClientBenchmarks public OpenFeatureClientBenchmarks() { var fixture = new Fixture(); - _clientName = fixture.Create(); - _clientVersion = fixture.Create(); - _flagName = fixture.Create(); - _defaultBoolValue = fixture.Create(); - _defaultStringValue = fixture.Create(); - _defaultIntegerValue = fixture.Create(); - _defaultDoubleValue = fixture.Create(); - _defaultStructureValue = fixture.Create(); - _emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - - _client = Api.Instance.GetClient(_clientName, _clientVersion); + this._clientName = fixture.Create(); + this._clientVersion = fixture.Create(); + this._flagName = fixture.Create(); + this._defaultBoolValue = fixture.Create(); + this._defaultStringValue = fixture.Create(); + this._defaultIntegerValue = fixture.Create(); + this._defaultDoubleValue = fixture.Create(); + this._defaultStructureValue = fixture.Create(); + this._emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); + + this._client = Api.Instance.GetClient(this._clientName, this._clientVersion); } [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue); + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty); + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValueAsync(_flagName, _defaultStringValue); + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty); + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue); + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty); + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue); + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty); + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValueAsync(_flagName, _defaultStructureValue); + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty); + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty, this._emptyFlagOptions); } } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index a50f3945..d0870ec3 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -41,7 +41,7 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) [Given(@"a provider is registered")] public void GivenAProviderIsRegistered() { - var memProvider = new InMemoryProvider(e2eFlagConfig); + var memProvider = new InMemoryProvider(this.e2eFlagConfig); Api.Instance.SetProviderAsync(memProvider).Wait(); client = Api.Instance.GetClient("TestClient", "1.0.0"); } @@ -204,9 +204,9 @@ public void Whencontextcontainskeyswithvalues(string field1, string field2, stri [When(@"a flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - contextAwareFlagKey = flagKey; - contextAwareDefaultValue = defaultValue; - contextAwareValue = client?.GetStringValueAsync(flagKey, contextAwareDefaultValue, context)?.Result; + this.contextAwareFlagKey = flagKey; + this.contextAwareDefaultValue = defaultValue; + this.contextAwareValue = client?.GetStringValueAsync(flagKey, this.contextAwareDefaultValue, this.context)?.Result; } [Then(@"the resolved string response should be ""(.*)""")] @@ -218,7 +218,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey!, contextAwareDefaultValue!, EvaluationContext.Empty).Result; + string? emptyContextValue = client?.GetStringValueAsync(this.contextAwareFlagKey!, this.contextAwareDefaultValue!, EvaluationContext.Empty).Result; Assert.Equal(expected, emptyContextValue); } @@ -239,8 +239,8 @@ public void Thenthedefaultstringvalueshouldbereturned() [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), notFoundDetails?.Reason); - Assert.Equal(errorCode, notFoundDetails?.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), this.notFoundDetails?.Reason); + Assert.Equal(errorCode, this.notFoundDetails?.ErrorType.GetDescription()); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] @@ -260,8 +260,8 @@ public void Thenthedefaultintegervalueshouldbereturned() [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), typeErrorDetails?.Reason); - Assert.Equal(errorCode, typeErrorDetails?.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), this.typeErrorDetails?.Reason); + Assert.Equal(errorCode, this.typeErrorDetails?.ErrorType.GetDescription()); } private IDictionary e2eFlagConfig = new Dictionary(){ diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 1df3c976..2f778ada 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -103,8 +103,8 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def var defaultClient = openFeature.GetProviderMetadata(); var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); - defaultClient.Name.Should().Be(NoOpProvider.NoOpProviderName); - namedClient.Name.Should().Be(TestProvider.DefaultName); + defaultClient?.Name.Should().Be(NoOpProvider.NoOpProviderName); + namedClient?.Name.Should().Be(TestProvider.DefaultName); } [Fact] @@ -117,7 +117,7 @@ public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() var defaultClient = openFeature.GetProviderMetadata(); - defaultClient.Name.Should().Be(TestProvider.DefaultName); + defaultClient?.Name.Should().Be(TestProvider.DefaultName); } [Fact] @@ -130,7 +130,7 @@ public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() await openFeature.SetProviderAsync(name, new TestProvider()); await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); - openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); + openFeature.GetProviderMetadata(name)?.Name.Should().Be(NoOpProvider.NoOpProviderName); } [Fact] @@ -187,7 +187,7 @@ public async Task OpenFeature_Should_Get_Metadata() var metadata = openFeature.GetProviderMetadata(); metadata.Should().NotBeNull(); - metadata.Name.Should().Be(NoOpProvider.NoOpProviderName); + metadata?.Name.Should().Be(NoOpProvider.NoOpProviderName); } [Theory] diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index a4fe51a4..7a1dff10 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -103,10 +103,10 @@ public override Task> ResolveStructureValueAsync(string public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { - await Task.Delay(initDelay).ConfigureAwait(false); - if (initException != null) + await Task.Delay(this.initDelay).ConfigureAwait(false); + if (this.initException != null) { - throw initException; + throw this.initException; } } From 15189f1c6f7eb0931036e022eed68f58a1110b5b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:39:38 +1000 Subject: [PATCH 186/316] chore(deps): update actions/upload-artifact action to v4.3.4 (#278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/upload-artifact](https://togithub.com/actions/upload-artifact) | action | patch | `v4.3.3` -> `v4.3.4` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.3.4`](https://togithub.com/actions/upload-artifact/releases/tag/v4.3.4) [Compare Source](https://togithub.com/actions/upload-artifact/compare/v4.3.3...v4.3.4) ##### What's Changed - Update [@​actions/artifact](https://togithub.com/actions/artifact) version, bump dependencies by [@​robherley](https://togithub.com/robherley) in [https://github.com/actions/upload-artifact/pull/584](https://togithub.com/actions/upload-artifact/pull/584) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4.3.3...v4.3.4
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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://www.mend.io/free-developer-tools/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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 893834b9..98d1ade6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.4 with: name: nupkgs path: src/**/*.nupkg From fb1cc66440dd6bdbbef1ac1f85bf3228b80073af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:46:16 +1000 Subject: [PATCH 187/316] chore(deps): update xunit-dotnet monorepo (#279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [xunit](https://togithub.com/xunit/xunit) | `2.8.1` -> `2.9.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [xunit.runner.visualstudio](https://togithub.com/xunit/visualstudio.xunit) | `2.8.1` -> `2.8.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit.runner.visualstudio/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit.runner.visualstudio/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit.runner.visualstudio/2.8.1/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit.runner.visualstudio/2.8.1/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
xunit/xunit (xunit) ### [`v2.9.0`](https://togithub.com/xunit/xunit/compare/2.8.1...2.9.0) [Compare Source](https://togithub.com/xunit/xunit/compare/2.8.1...2.9.0)
xunit/visualstudio.xunit (xunit.runner.visualstudio) ### [`v2.8.2`](https://togithub.com/xunit/visualstudio.xunit/compare/2.8.1...2.8.2) [Compare Source](https://togithub.com/xunit/visualstudio.xunit/compare/2.8.1...2.8.2)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/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> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3ceb087b..b443a8af 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,8 +24,8 @@ - - + + From 871dcacc94fa2abb10434616c469cad6f674f07a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:51:40 +1000 Subject: [PATCH 188/316] chore(deps): update dependency dotnet-sdk to v8.0.303 (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dotnet-sdk](https://togithub.com/dotnet/sdk) | dotnet-sdk | patch | `8.0.301` -> `8.0.303` | --- ### Release Notes
dotnet/sdk (dotnet-sdk) ### [`v8.0.303`](https://togithub.com/dotnet/sdk/compare/v8.0.302...v8.0.303) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.302...v8.0.303) ### [`v8.0.302`](https://togithub.com/dotnet/sdk/compare/v8.0.301...v8.0.302) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.301...v8.0.302)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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://www.mend.io/free-developer-tools/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> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 635d63fc..9f8f3618 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.301" + "version": "8.0.303" } } From 2dbe1f4c95aeae501c8b5154b1ccefafa7df2632 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:59:13 +1000 Subject: [PATCH 189/316] feat: Drop net7 TFM (#284) ## This PR .net7 was EOL on May 14, 2024 https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> --- .github/workflows/ci.yml | 2 -- .github/workflows/code-coverage.yml | 1 - .github/workflows/e2e.yml | 1 - .github/workflows/release.yml | 1 - src/OpenFeature/OpenFeature.csproj | 2 +- test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj | 2 +- test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj | 2 +- test/OpenFeature.Tests/OpenFeature.Tests.csproj | 2 +- 8 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98d1ade6..bc57c30b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json @@ -68,7 +67,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 1f07ffc6..83d837eb 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -30,7 +30,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2cc0a84f..914d6809 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -25,7 +25,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d8aa265..b51c9bff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index 9e272ba2..ed991c4e 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net6.0;net7.0;net8.0;net462 + netstandard2.0;net6.0;net8.0;net462 OpenFeature README.md diff --git a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj index 81342e09..974dce5c 100644 --- a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj +++ b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 OpenFeature.Benchmark Exe diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 757c4e8f..d91b338e 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 $(TargetFrameworks);net462 OpenFeature.E2ETests diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 9ceac0dc..bfadbf9b 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 $(TargetFrameworks);net462 OpenFeature.Tests From 2f8bd2179ec35f79cbbab77206de78dd9b0f58d6 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:26:34 +1000 Subject: [PATCH 190/316] fix: Should map metadata when converting from ResolutionDetails to FlagEvaluationDetails (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR When converting the ResolutionDetails to FlagEvalutionDetails we aren't passing the ImmutableMetadata to the new object. ### Related Issues Fixes [#281](https://github.com/open-feature/dotnet-sdk/issues/281) ### Notes This PR is done on a common merge base so we can merge it into v1 as well ### Follow-up Tasks N/A ### How to test Unit test added to covert the missing test case --------- Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .../Extension/ResolutionDetailsExtensions.cs | 2 +- .../OpenFeatureClientTests.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs index 616e530a..f38356ad 100644 --- a/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs +++ b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs @@ -7,7 +7,7 @@ internal static class ResolutionDetailsExtensions public static FlagEvaluationDetails ToFlagEvaluationDetails(this ResolutionDetails details) { return new FlagEvaluationDetails(details.FlagKey, details.Value, details.ErrorType, details.Reason, - details.Variant, details.ErrorMessage); + details.Variant, details.ErrorMessage, details.FlagMetadata); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 925de66a..d1a91c1f 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -11,6 +12,7 @@ using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; using OpenFeature.Error; +using OpenFeature.Extension; using OpenFeature.Model; using OpenFeature.Tests.Internal; using Xunit; @@ -480,5 +482,27 @@ public void Should_Get_And_Set_Context() client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } + + + [Fact] + public void ToFlagEvaluationDetails_Should_Convert_All_Properties() + { + var fixture = new Fixture(); + var flagName = fixture.Create(); + var boolValue = fixture.Create(); + var errorType = fixture.Create(); + var reason = fixture.Create(); + var variant = fixture.Create(); + var errorMessage = fixture.Create(); + var flagData = fixture + .CreateMany>(10) + .ToDictionary(x => x.Key, x => x.Value); + var flagMetadata = new ImmutableMetadata(flagData); + + var expected = new ResolutionDetails(flagName, boolValue, errorType, reason, variant, errorMessage, flagMetadata); + var result = expected.ToFlagEvaluationDetails(); + + result.Should().BeEquivalentTo(expected); + } } } From ccc2f7fbd4e4f67eb03c2e6a07140ca31225da2c Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 29 Jul 2024 13:53:15 -0400 Subject: [PATCH 191/316] feat: back targetingKey with internal map (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the internal dictionary for the `targetingKey`. This is non-breaking from a compiler perspective. It could result in some behavioral changes, but IMO they are largely desirable. Fixes: https://github.com/open-feature/dotnet-sdk/issues/235 --------- Signed-off-by: Todd Baert Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Model/EvaluationContext.cs | 23 +++++--- .../Model/EvaluationContextBuilder.cs | 23 +------- .../OpenFeatureEvaluationContextTests.cs | 52 ++++++++++++++++++- 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs index 59b1fe20..304e4cd9 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -11,26 +11,30 @@ namespace OpenFeature.Model /// Evaluation context public sealed class EvaluationContext { + /// + /// 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. /// - /// The targeting key - /// The content of the context. - internal EvaluationContext(string? targetingKey, Structure content) + /// + internal EvaluationContext(Structure content) { - this.TargetingKey = targetingKey; this._structure = content; } + /// /// Private constructor for making an empty . /// private EvaluationContext() { this._structure = Structure.Empty; - this.TargetingKey = string.Empty; } /// @@ -89,7 +93,14 @@ public IImmutableDictionary AsDictionary() /// /// Returns the targeting key for the context. /// - public string? TargetingKey { get; } + public string? TargetingKey + { + get + { + this._structure.TryGetValue(TargetingKeyIndex, out Value? targetingKey); + return targetingKey?.AsString; + } + } /// /// Return an enumerator for all values diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs index c672c401..30e2ffe0 100644 --- a/src/OpenFeature/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -14,8 +14,6 @@ public sealed class EvaluationContextBuilder { private readonly StructureBuilder _attributes = Structure.Builder(); - internal string? TargetingKey { get; private set; } - /// /// Internal to only allow direct creation by . /// @@ -28,7 +26,7 @@ internal EvaluationContextBuilder() { } /// This builder public EvaluationContextBuilder SetTargetingKey(string targetingKey) { - this.TargetingKey = targetingKey; + this._attributes.Set(EvaluationContext.TargetingKeyIndex, targetingKey); return this; } @@ -138,23 +136,6 @@ public EvaluationContextBuilder Set(string key, DateTime value) /// This builder public EvaluationContextBuilder Merge(EvaluationContext context) { - string? newTargetingKey = ""; - - if (!string.IsNullOrWhiteSpace(this.TargetingKey)) - { - newTargetingKey = this.TargetingKey; - } - - if (!string.IsNullOrWhiteSpace(context.TargetingKey)) - { - newTargetingKey = context.TargetingKey; - } - - if (!string.IsNullOrWhiteSpace(newTargetingKey)) - { - this.TargetingKey = newTargetingKey; - } - foreach (var kvp in context) { this.Set(kvp.Key, kvp.Value); @@ -169,7 +150,7 @@ public EvaluationContextBuilder Merge(EvaluationContext context) /// An immutable public EvaluationContext Build() { - return new EvaluationContext(this.TargetingKey, this._attributes.Build()); + return new EvaluationContext(this._attributes.Build()); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 5329620f..826ac68e 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -160,7 +160,7 @@ public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() var key = "testKey"; var expectedValue = new Value("testValue"); var structure = new Structure(new Dictionary { { key, expectedValue } }); - var evaluationContext = new EvaluationContext("targetingKey", structure); + var evaluationContext = new EvaluationContext(structure); // Act var result = evaluationContext.TryGetValue(key, out var actualValue); @@ -169,5 +169,55 @@ public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() Assert.True(result); Assert.Equal(expectedValue, actualValue); } + + [Fact] + public void GetValueOnTargetingKeySetWithTargetingKey_Equals_TargetingKey() + { + // Arrange + var value = "my_targeting_key"; + var evaluationContext = EvaluationContext.Builder().SetTargetingKey(value).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Equal(value, actualFromStructure?.AsString); + Assert.Equal(value, actualFromTargetingKey); + } + + [Fact] + public void GetValueOnTargetingKeySetWithStructure_Equals_TargetingKey() + { + // Arrange + var value = "my_targeting_key"; + var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(value)).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Equal(value, actualFromStructure?.AsString); + Assert.Equal(value, actualFromTargetingKey); + } + + [Fact] + public void GetValueOnTargetingKeySetWithNonStringValue_Equals_Null() + { + // Arrange + var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(1)).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Null(actualFromStructure?.AsString); + Assert.Null(actualFromTargetingKey); + } } } From 00e99d6c2208b304748d00a931f460d6d6aab4de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:20:59 +1000 Subject: [PATCH 192/316] chore(deps): update actions/upload-artifact action to v4.3.5 (#291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/upload-artifact](https://togithub.com/actions/upload-artifact) | action | patch | `v4.3.4` -> `v4.3.5` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.3.5`](https://togithub.com/actions/upload-artifact/compare/v4.3.4...v4.3.5) [Compare Source](https://togithub.com/actions/upload-artifact/compare/v4.3.4...v4.3.5)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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://www.mend.io/free-developer-tools/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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc57c30b..13092d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: nupkgs path: src/**/*.nupkg From 4c0592e6baf86d831fc7b39762c960ca0dd843a9 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 13 Aug 2024 14:16:16 -0400 Subject: [PATCH 193/316] feat!: domain instead of client name (#294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses "domain" terminology instead of "client name / named client". Fixes: https://github.com/open-feature/dotnet-sdk/issues/249 I believe with this, we are able to release a 2.0 --------- Signed-off-by: Todd Baert Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- README.md | 38 ++++++------ src/OpenFeature/Api.cs | 32 +++++----- src/OpenFeature/ProviderRepository.cs | 22 +++---- .../OpenFeatureClientBenchmarks.cs | 6 +- .../OpenFeatureClientTests.cs | 60 +++++++++---------- .../OpenFeatureEventTests.cs | 8 +-- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 4 +- test/OpenFeature.Tests/OpenFeatureTests.cs | 4 +- 8 files changed, 89 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 6844915f..bce047f8 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,16 @@ public async Task Example() ## 🌟 Features -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| βœ… | [Logging](#logging) | Integrate with popular logging packages. | -| βœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| Status | Features | Description | +| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Logging](#logging) | Integrate with popular logging packages. | +| βœ… | [Domains](#domains) | Logically bind clients with providers. | +| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | > Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ @@ -96,7 +96,7 @@ await Api.Instance.SetProviderAsync(new MyProvider()); ``` In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more detail below. +This is possible using [domains](#domains), which is covered in more detail below. ### Targeting @@ -151,27 +151,29 @@ var value = await client.GetBooleanValueAsync("boolFlag", false, context, new Fl The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. -### Named clients +### Domains -Clients can be given a name. -A name is a logical identifier that can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. +Clients can be assigned to a domain. +A domain is a logical identifier which can be used to associate clients with a particular provider. +If a domain has no associated provider, the default provider is used. ```csharp // registering the default provider await Api.Instance.SetProviderAsync(new LocalProvider()); -// registering a named provider +// registering a provider to a domain await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); // a client backed by default provider FeatureClient clientDefault = Api.Instance.GetClient(); // a client backed by CachedProvider -FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); - +FeatureClient scopedClient = Api.Instance.GetClient("clientForCache"); ``` +Domains can be defined on a provider during registration. +For more details, please refer to the [providers](#providers) section. + ### Eventing Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 3fa38916..fae9916b 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -50,19 +50,19 @@ public async Task SetProviderAsync(FeatureProvider featureProvider) } /// - /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and + /// 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. /// - /// Name of client + /// An identifier which logically binds clients with providers /// Implementation of - public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider) + public async Task SetProviderAsync(string domain, FeatureProvider featureProvider) { - if (string.IsNullOrWhiteSpace(clientName)) + if (string.IsNullOrWhiteSpace(domain)) { - throw new ArgumentNullException(nameof(clientName)); + throw new ArgumentNullException(nameof(domain)); } - this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); + this._eventExecutor.RegisterClientFeatureProvider(domain, featureProvider); + await this._repository.SetProviderAsync(domain, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); } /// @@ -82,14 +82,15 @@ public FeatureProvider GetProvider() } /// - /// Gets the feature provider with given clientName + /// Gets the feature provider with given domain /// - /// Name of client - /// A provider associated with the given clientName, if clientName is empty or doesn't + /// 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 clientName) + public FeatureProvider GetProvider(string domain) { - return this._repository.GetProvider(clientName); + return this._repository.GetProvider(domain); } /// @@ -104,12 +105,13 @@ public FeatureProvider GetProvider(string clientName) public Metadata? GetProviderMetadata() => this.GetProvider().GetMetadata(); /// - /// Gets providers metadata assigned to the given clientName. If the clientName has no provider + /// Gets providers metadata assigned to the given domain. If the domain has no provider /// assigned to it the default provider will be returned /// - /// Name of client + /// An identifier which logically binds clients with providers + /// Metadata assigned to provider - public Metadata? GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); + public Metadata? GetProviderMetadata(string domain) => this.GetProvider(domain).GetMetadata(); /// /// Create a new instance of using the current provider diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 760503b6..49f1de43 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -127,7 +127,7 @@ private static async Task InitProviderAsync( /// /// Set a named provider /// - /// the name to associate with the provider + /// an identifier which logically binds clients with providers /// the provider to set as the default, passing null has no effect /// the context to initialize the provider with /// @@ -138,15 +138,15 @@ private static async Task InitProviderAsync( /// initialization /// /// The to cancel any async side effects. - public async Task SetProviderAsync(string? clientName, + public async Task SetProviderAsync(string? domain, FeatureProvider? featureProvider, EvaluationContext context, Func? afterInitSuccess = null, Func? afterInitError = null, CancellationToken cancellationToken = default) { - // Cannot set a provider for a null clientName. - if (clientName == null) + // Cannot set a provider for a null domain. + if (domain == null) { return; } @@ -155,17 +155,17 @@ public async Task SetProviderAsync(string? clientName, try { - this._featureProviders.TryGetValue(clientName, out var oldProvider); + this._featureProviders.TryGetValue(domain, out var oldProvider); if (featureProvider != null) { - this._featureProviders.AddOrUpdate(clientName, featureProvider, + this._featureProviders.AddOrUpdate(domain, featureProvider, (key, current) => featureProvider); } else { // If names of clients are programmatic, then setting the provider to null could result // in unbounded growth of the collection. - this._featureProviders.TryRemove(clientName, out _); + this._featureProviders.TryRemove(domain, out _); } // We want to allow shutdown to happen concurrently with initialization, and the caller to not @@ -238,22 +238,22 @@ public FeatureProvider GetProvider() } } - public FeatureProvider GetProvider(string? clientName) + public FeatureProvider GetProvider(string? domain) { #if NET6_0_OR_GREATER - if (string.IsNullOrEmpty(clientName)) + if (string.IsNullOrEmpty(domain)) { return this.GetProvider(); } #else // This is a workaround for the issue in .NET Framework where string.IsNullOrEmpty is not nullable compatible. - if (clientName == null || string.IsNullOrEmpty(clientName)) + if (domain == null || string.IsNullOrEmpty(domain)) { return this.GetProvider(); } #endif - return this._featureProviders.TryGetValue(clientName, out var featureProvider) + return this._featureProviders.TryGetValue(domain, out var featureProvider) ? featureProvider : this.GetProvider(); } diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 3796821e..03650144 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -16,7 +16,7 @@ namespace OpenFeature.Benchmark [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureClientBenchmarks { - private readonly string _clientName; + private readonly string _domain; private readonly string _clientVersion; private readonly string _flagName; private readonly bool _defaultBoolValue; @@ -30,7 +30,7 @@ public class OpenFeatureClientBenchmarks public OpenFeatureClientBenchmarks() { var fixture = new Fixture(); - this._clientName = fixture.Create(); + this._domain = fixture.Create(); this._clientVersion = fixture.Create(); this._flagName = fixture.Create(); this._defaultBoolValue = fixture.Create(); @@ -40,7 +40,7 @@ public OpenFeatureClientBenchmarks() this._defaultStructureValue = fixture.Create(); this._emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - this._client = Api.Instance.GetClient(this._clientName, this._clientVersion); + this._client = Api.Instance.GetClient(this._domain, this._clientVersion); } [Benchmark] diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index d1a91c1f..ce3e9e93 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -27,13 +27,13 @@ public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture public void OpenFeatureClient_Should_Allow_Hooks() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var hook1 = Substitute.For(); var hook2 = Substitute.For(); var hook3 = Substitute.For(); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); client.AddHooks(new[] { hook1, hook2 }); @@ -53,11 +53,11 @@ public void OpenFeatureClient_Should_Allow_Hooks() public void OpenFeatureClient_Metadata_Should_Have_Name() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); - client.GetMetadata().Name.Should().Be(clientName); + client.GetMetadata().Name.Should().Be(domain); client.GetMetadata().Version.Should().Be(clientVersion); } @@ -68,7 +68,7 @@ public void OpenFeatureClient_Metadata_Should_Have_Name() public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultBoolValue = fixture.Create(); @@ -79,7 +79,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetBooleanValueAsync(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); @@ -114,7 +114,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultBoolValue = fixture.Create(); @@ -125,7 +125,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); @@ -163,7 +163,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatch() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -176,7 +176,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(mockedFeatureProvider); - var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger); + var client = Api.Instance.GetClient(domain, clientVersion, mockedLogger); var evaluationDetails = await client.GetObjectDetailsAsync(flagName, defaultValue); evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); @@ -277,7 +277,7 @@ public async Task Must_Short_Circuit_Fatal() public async Task Should_Resolve_BooleanValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -288,7 +288,7 @@ public async Task Should_Resolve_BooleanValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetBooleanValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -299,7 +299,7 @@ public async Task Should_Resolve_BooleanValue() public async Task Should_Resolve_StringValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -310,7 +310,7 @@ public async Task Should_Resolve_StringValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetStringValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -321,7 +321,7 @@ public async Task Should_Resolve_StringValue() public async Task Should_Resolve_IntegerValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -332,7 +332,7 @@ public async Task Should_Resolve_IntegerValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetIntegerValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -343,7 +343,7 @@ public async Task Should_Resolve_IntegerValue() public async Task Should_Resolve_DoubleValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -354,7 +354,7 @@ public async Task Should_Resolve_DoubleValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetDoubleValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -365,7 +365,7 @@ public async Task Should_Resolve_DoubleValue() public async Task Should_Resolve_StructureValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -376,7 +376,7 @@ public async Task Should_Resolve_StructureValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetObjectValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -387,7 +387,7 @@ public async Task Should_Resolve_StructureValue() public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -399,7 +399,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); @@ -412,7 +412,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -424,7 +424,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); @@ -437,7 +437,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() public async Task Cancellation_Token_Added_Is_Passed_To_Provider() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultString = fixture.Create(); @@ -459,8 +459,8 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(clientName, featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + await Api.Instance.SetProviderAsync(domain, featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); var task = client.GetStringDetailsAsync(flagName, defaultString, EvaluationContext.Empty, null, cts.Token); cts.Cancel(); // cancel before awaiting @@ -474,11 +474,11 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() public void Should_Get_And_Set_Context() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var KEY = "key"; var VAL = 1; - FeatureClient client = Api.Instance.GetClient(clientName, clientVersion); + FeatureClient client = Api.Instance.GetClient(domain, clientVersion); client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index a7bcd2e7..a4b0d111 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -304,9 +304,9 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered() var fixture = new Fixture(); var eventHandler = Substitute.For(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); - var myClient = Api.Instance.GetClient(clientName, clientVersion); + var myClient = Api.Instance.GetClient(domain, clientVersion); var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); @@ -332,9 +332,9 @@ public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Hand failingEventHandler.When(x => x.Invoke(Arg.Any())) .Do(x => throw new Exception()); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); - var myClient = Api.Instance.GetClient(clientName, clientVersion); + var myClient = Api.Instance.GetClient(domain, clientVersion); myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 9ca5b364..cc8b08a1 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -27,7 +27,7 @@ public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture public async Task Hooks_Should_Be_Called_In_Order() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -54,7 +54,7 @@ public async Task Hooks_Should_Be_Called_In_Order() testProvider.AddHook(providerHook); Api.Instance.AddHooks(apiHook); await Api.Instance.SetProviderAsync(testProvider); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); client.AddHooks(clientHook); await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empty, diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 2f778ada..acc53b61 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -101,10 +101,10 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()); var defaultClient = openFeature.GetProviderMetadata(); - var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); + var domainScopedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); defaultClient?.Name.Should().Be(NoOpProvider.NoOpProviderName); - namedClient?.Name.Should().Be(TestProvider.DefaultName); + domainScopedClient?.Name.Should().Be(TestProvider.DefaultName); } [Fact] From aec222fe1b1a5b52f8349ceb98c12b636eb155eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:01:43 -0400 Subject: [PATCH 194/316] chore(deps): update dependency benchmarkdotnet to v0.14.0 (#293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [BenchmarkDotNet](https://togithub.com/dotnet/BenchmarkDotNet) | `0.13.1` -> `0.14.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/BenchmarkDotNet/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/BenchmarkDotNet/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/BenchmarkDotNet/0.13.1/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/BenchmarkDotNet/0.13.1/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
dotnet/BenchmarkDotNet (BenchmarkDotNet) ### [`v0.14.0`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.14.0): 0.14.0 Full changelog: https://benchmarkdotnet.org/changelog/v0.14.0.html #### Highlights - Introduce `BenchmarkDotNet.Diagnostics.dotMemory` [#​2549](https://togithub.com/dotnet/BenchmarkDotNet/pull/2549): memory allocation profile of your benchmarks using [dotMemory](https://www.jetbrains.com/dotmemory/), see [@​BenchmarkDotNet](https://togithub.com/BenchmarkDotNet).Samples.IntroDotMemoryDiagnoser - Introduce `BenchmarkDotNet.Exporters.Plotting` [#​2560](https://togithub.com/dotnet/BenchmarkDotNet/pull/2560): plotting via [ScottPlot](https://scottplot.net/) (initial version) - Multiple bugfixes - The default build toolchains have been updated to pass `IntermediateOutputPath`, `OutputPath`, and `OutDir` properties to the `dotnet build` command. This change forces all build outputs to be placed in a new directory generated by BenchmarkDotNet, and fixes many issues that have been reported with builds. You can also access these paths in your own `.csproj` and `.props` from those properties if you need to copy custom files to the output. #### Bug fixes - Fixed multiple build-related bugs including passing MsBuildArguments and .Net 8's `UseArtifactsOutput`. #### Breaking Changes - `DotNetCliBuilder` removed `retryFailedBuildWithNoDeps` constructor option. - `DotNetCliCommand` removed `RetryFailedBuildWithNoDeps` property and `BuildNoRestoreNoDependencies()` and `PublishNoBuildAndNoRestore()` methods (replaced with `PublishNoRestore()`). ### [`v0.13.12`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.12): 0.13.12 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.12.html #### Highlights The biggest highlight of this release if our new VSTest Adapter, which allows to run benchmarks as unit tests in your favorite IDE! The detailed guide can be found [here](https://benchmarkdotnet.org/articles/features/vstest.html). This release also includes to a minor bug fix that caused incorrect job id generation: fixed job id generation ([#​2491](https://togithub.com/dotnet/BenchmarkDotNet/pull/2491)). Also, the target framework in the BenchmarkDotNet templates was bumped to .NET 8.0. ### [`v0.13.11`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.11): 0.13.11 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.11.html In the [v0.13.11](https://togithub.com/dotnet/BenchmarkDotNet/issues?q=milestone:v0.13.11) scope, 4 issues were resolved and 8 pull requests were merged. This release includes 28 commits by 7 contributors. #### Resolved issues (4) - [#​2060](https://togithub.com/dotnet/BenchmarkDotNet/issues/2060) NativeAOT benchmark started from .Net Framework host doesn't have all intrinsics enabled (assignee: [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2233](https://togithub.com/dotnet/BenchmarkDotNet/issues/2233) Q: Include hardware counters in XML output (assignee: [@​nazulg](https://togithub.com/nazulg)) - [#​2388](https://togithub.com/dotnet/BenchmarkDotNet/issues/2388) Include AVX512 in listed HardwareIntrinsics - [#​2463](https://togithub.com/dotnet/BenchmarkDotNet/issues/2463) Bug. Native AOT .NET 7.0 doesn't work. System.NotSupportedException: X86Serialize (assignee: [@​adamsitnik](https://togithub.com/adamsitnik)) #### Merged pull requests (8) - [#​2412](https://togithub.com/dotnet/BenchmarkDotNet/pull/2412) Add HardwareIntrinsics AVX-512 info (by [@​nietras](https://togithub.com/nietras)) - [#​2458](https://togithub.com/dotnet/BenchmarkDotNet/pull/2458) Adds Metrics Columns to Benchmark Report Output (by [@​nazulg](https://togithub.com/nazulg)) - [#​2459](https://togithub.com/dotnet/BenchmarkDotNet/pull/2459) Enable MemoryDiagnoser on Legacy Mono (by [@​MichalPetryka](https://togithub.com/MichalPetryka)) - [#​2462](https://togithub.com/dotnet/BenchmarkDotNet/pull/2462) update SDK to .NET 8 (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2464](https://togithub.com/dotnet/BenchmarkDotNet/pull/2464) Use "native" for .NET 8, don't use "serialize" for .NET 7 (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2465](https://togithub.com/dotnet/BenchmarkDotNet/pull/2465) fix NativeAOT toolchain and tests (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2468](https://togithub.com/dotnet/BenchmarkDotNet/pull/2468) Add OperationsPerSecondAttribute (by [@​DarkWanderer](https://togithub.com/DarkWanderer)) - [#​2475](https://togithub.com/dotnet/BenchmarkDotNet/pull/2475) Fix some tests (by [@​timcassell](https://togithub.com/timcassell)) #### Commits (28) - [bb55e6](https://togithub.com/dotnet/BenchmarkDotNet/commit/bb55e6b067829c74e04838255e96d949857d5731) Set next BenchmarkDotNet version: 0.13.11 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [db4d8b](https://togithub.com/dotnet/BenchmarkDotNet/commit/db4d8b6d8a652db4bb1e4b1b4b0cd9df917e9584) Adds Metrics Columns to Benchmark Report Output ([#​2458](https://togithub.com/dotnet/BenchmarkDotNet/issues/2458)) (by [@​nazulg](https://togithub.com/nazulg)) - [e93b2b](https://togithub.com/dotnet/BenchmarkDotNet/commit/e93b2b1b332fc90da4934025e2edba7d67a15b54) Use "native" for .NET 8, don't use "serialize" for .NET 7 ([#​2464](https://togithub.com/dotnet/BenchmarkDotNet/issues/2464)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [127157](https://togithub.com/dotnet/BenchmarkDotNet/commit/127157924014afe2d0b58398d682381a855d7c34) \[build] Fix spellcheck-docs workflow (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8a02ec](https://togithub.com/dotnet/BenchmarkDotNet/commit/8a02ec28d55529f9be0ea66d843049738b2be8fa) \[build] Use our .NET SDK on Windows (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [1b39e8](https://togithub.com/dotnet/BenchmarkDotNet/commit/1b39e8e6d5437bdbf0bb62986e680e54b19cc873) Suppress NU1903 in IntegrationTests.ManualRunning.MultipleFrameworks (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [e90311](https://togithub.com/dotnet/BenchmarkDotNet/commit/e90311539d78e4bf9d90c6aeae9f40219b31a4ac) update SDK to .NET 8 ([#​2462](https://togithub.com/dotnet/BenchmarkDotNet/issues/2462)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [fc7afe](https://togithub.com/dotnet/BenchmarkDotNet/commit/fc7afeddcff7a52ccee165ac99ba216e8eb138ab) Enable MemoryDiagnoser on Legacy Mono ([#​2459](https://togithub.com/dotnet/BenchmarkDotNet/issues/2459)) (by [@​MichalPetryka](https://togithub.com/MichalPetryka)) - [630622](https://togithub.com/dotnet/BenchmarkDotNet/commit/630622b6df3192f766ffa03ff07b5086e70cb264) fix NativeAOT toolchain and tests ([#​2465](https://togithub.com/dotnet/BenchmarkDotNet/issues/2465)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [536a28](https://togithub.com/dotnet/BenchmarkDotNet/commit/536a28e0ff2196255fb120aa0d39e40bdbde454a) Add HardwareIntrinsics AVX-512 info ([#​2412](https://togithub.com/dotnet/BenchmarkDotNet/issues/2412)) (by [@​nietras](https://togithub.com/nietras)) - [3fa045](https://togithub.com/dotnet/BenchmarkDotNet/commit/3fa0456495cac82b536902b101a2972c62c3e4a8) Add OperationsPerSecondAttribute ([#​2468](https://togithub.com/dotnet/BenchmarkDotNet/issues/2468)) (by [@​DarkWanderer](https://togithub.com/DarkWanderer)) - [0583cb](https://togithub.com/dotnet/BenchmarkDotNet/commit/0583cb90739b3ee4b8258f807ef42cdc3243f82f) Bump Microsoft.NETCore.Platforms: 5.0.0->6.0.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [2e62b9](https://togithub.com/dotnet/BenchmarkDotNet/commit/2e62b9b0a8c80255914e9e11d06d92871df40f85) Remove netcoreapp2.0;net461 from TFMs for IntegrationTests.ManualRunning.Mult... (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [92fa3f](https://togithub.com/dotnet/BenchmarkDotNet/commit/92fa3f834e0519d32fd8fc97e26aa82f9626b241) Bump xunit: 2.5.0->2.6.2 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [01e220](https://togithub.com/dotnet/BenchmarkDotNet/commit/01e2201c826dd44e089a22c40d8c3abecba320fa) Bump xunit.runner.visualstudio: 2.5.0->2.5.4 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [29a94c](https://togithub.com/dotnet/BenchmarkDotNet/commit/29a94ce301dac6121d1e0d1a0d783a6491c27703) Bump Verify.Xunit: 20.3.2->20.8.2 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [538e0e](https://togithub.com/dotnet/BenchmarkDotNet/commit/538e0e1771be037ef587b08cb52515ce6daf5c0e) Bump Microsoft.NET.Test.SDK: 17.6.2->17.8.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [136e4b](https://togithub.com/dotnet/BenchmarkDotNet/commit/136e4bb3f18a419df40c18a5430a29243ab57fd8) Remove explicit Microsoft.NETFramework.ReferenceAssemblies reference in Bench... (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [423b84](https://togithub.com/dotnet/BenchmarkDotNet/commit/423b8473d02d5bd59617675190660222198bf7d0) \[build] Bump Docfx.App: 2.71.1->2.74.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [718953](https://togithub.com/dotnet/BenchmarkDotNet/commit/718953674a83da4de6563368f38776048024f0d3) \[build] Bump Octokit: 7.0.0->9.0.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [0cce91](https://togithub.com/dotnet/BenchmarkDotNet/commit/0cce9120bd717e31a4a6a4a396faa8f38fd3cc08) \[build] Bump Cake.Frosting: 3.2.0->4.0.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [4d5dc9](https://togithub.com/dotnet/BenchmarkDotNet/commit/4d5dc9ca13072d384cabf565bc3622f8de5626d7) Fix Newtonsoft.Json v13.0.1 in BenchmarkDotNet.IntegrationTests (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [c7ec60](https://togithub.com/dotnet/BenchmarkDotNet/commit/c7ec60ad6d4e54a99463eb46a0307196cc040940) Enable UserCanSpecifyCustomNuGetPackageDependency test on Linux (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [a572db](https://togithub.com/dotnet/BenchmarkDotNet/commit/a572db119798fb58b24437ccef6a364efd59e836) Bump C# LangVersion: 11.0->12.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [b4ac9d](https://togithub.com/dotnet/BenchmarkDotNet/commit/b4ac9df9f7890ca9669e2b9c8835af35c072a453) Nullability cleanup (2023-11-26) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [5557ae](https://togithub.com/dotnet/BenchmarkDotNet/commit/5557aee0638bda38001bd6c2000164d9b96d315a) \[build] Bump Docfx.App: 2.74.0->2.74.1 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [b987b9](https://togithub.com/dotnet/BenchmarkDotNet/commit/b987b99ed37455e5443ed03169890998c3152ae9) Fixed some tests. ([#​2475](https://togithub.com/dotnet/BenchmarkDotNet/issues/2475)) (by [@​timcassell](https://togithub.com/timcassell)) - [05eb00](https://togithub.com/dotnet/BenchmarkDotNet/commit/05eb00f3536061ca624bab3d9a4ca2f3c0be5922) Prepare v0.13.11 changelog (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Contributors (7) - Adam Sitnik ([@​adamsitnik](https://togithub.com/adamsitnik)) - Andrey Akinshin ([@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - MichaΕ‚ Petryka ([@​MichalPetryka](https://togithub.com/MichalPetryka)) - Nazul Grimaldo ([@​nazulg](https://togithub.com/nazulg)) - nietras ([@​nietras](https://togithub.com/nietras)) - Oleg V. Kozlyuk ([@​DarkWanderer](https://togithub.com/DarkWanderer)) - Tim Cassell ([@​timcassell](https://togithub.com/timcassell)) Thank you very much! ### [`v0.13.10`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.10): 0.13.10 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.10.html #### Highlights Initial support of .NET 9 and minor bug fixes. #### Details In the [v0.13.10](https://togithub.com/dotnet/BenchmarkDotNet/issues?q=milestone:v0.13.10) scope, 2 issues were resolved and 3 pull requests were merged. This release includes 10 commits by 4 contributors. #### Resolved issues (2) - [#​2436](https://togithub.com/dotnet/BenchmarkDotNet/issues/2436) BenchmarkDotNet Access Denied Error on WSL2 when Writing to '/mnt/c/DumpStack.log.tmp' (assignee: [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [#​2455](https://togithub.com/dotnet/BenchmarkDotNet/issues/2455) .NET 9 support (assignee: [@​adamsitnik](https://togithub.com/adamsitnik)) #### Merged pull requests (3) - [#​2447](https://togithub.com/dotnet/BenchmarkDotNet/pull/2447) Add support for wasm/net9.0 (by [@​radical](https://togithub.com/radical)) - [#​2453](https://togithub.com/dotnet/BenchmarkDotNet/pull/2453) feat: set RuntimeHostConfigurationOption on generated project (by [@​workgroupengineering](https://togithub.com/workgroupengineering)) - [#​2456](https://togithub.com/dotnet/BenchmarkDotNet/pull/2456) implement full .NET 9 support (by [@​adamsitnik](https://togithub.com/adamsitnik)) #### Commits (10) - [c27152](https://togithub.com/dotnet/BenchmarkDotNet/commit/c27152b9d7b6391501abcf7e8edcb2804999622f) Set next BenchmarkDotNet version: 0.13.10 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [2e96d2](https://togithub.com/dotnet/BenchmarkDotNet/commit/2e96d29453a804cfc1b92fffeea94c866522167a) Don't show AssemblyInformationalVersion metadata in BDN BrandVersion (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [d17c6a](https://togithub.com/dotnet/BenchmarkDotNet/commit/d17c6ad0bd8ac15d83ced0a7522de7dd51526ad4) Support Windows 11 23H2 (10.0.22631) in OsBrandStringHelper (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [af9c5c](https://togithub.com/dotnet/BenchmarkDotNet/commit/af9c5c6013b4e661cda0ff8fed40a50ae62d5a74) Exception handling in DotNetCliGenerator.GetRootDirectory, fix [#​2436](https://togithub.com/dotnet/BenchmarkDotNet/issues/2436) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [e11136](https://togithub.com/dotnet/BenchmarkDotNet/commit/e11136897bdf26c004076bcbe812bb4ae60f8859) \[build] Bump Docfx.App: 2.71.0->2.71.1 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [7b342f](https://togithub.com/dotnet/BenchmarkDotNet/commit/7b342f5cfb63c73708f3e69dde33d7430a3c0401) Add support for wasm/net9.0 ([#​2447](https://togithub.com/dotnet/BenchmarkDotNet/issues/2447)) (by [@​radical](https://togithub.com/radical)) - [e17068](https://togithub.com/dotnet/BenchmarkDotNet/commit/e170684208103ca5ba4212ad8dc7c2aad5cf02d4) Adjust 'Failed to set up high priority' message (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [0a734a](https://togithub.com/dotnet/BenchmarkDotNet/commit/0a734a94a13733c2950d7edbac08499c6f2c108a) feat: set RuntimeHostConfigurationOption on generated project ([#​2453](https://togithub.com/dotnet/BenchmarkDotNet/issues/2453)) (by [@​workgroupengineering](https://togithub.com/workgroupengineering)) - [ae4914](https://togithub.com/dotnet/BenchmarkDotNet/commit/ae49148a92c358676190772803fe0ed532814ce3) implement full .NET 9 support ([#​2456](https://togithub.com/dotnet/BenchmarkDotNet/issues/2456)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [40c414](https://togithub.com/dotnet/BenchmarkDotNet/commit/40c4142734ce68bdfcbccf7086ed2b724e9428bc) Prepare v0.13.10 changelog (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Contributors (4) - Adam Sitnik ([@​adamsitnik](https://togithub.com/adamsitnik)) - Andrey Akinshin ([@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - Ankit Jain ([@​radical](https://togithub.com/radical)) - workgroupengineering ([@​workgroupengineering](https://togithub.com/workgroupengineering)) Thank you very much! ### [`v0.13.9`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.9): 0.13.9 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.9.html In the [v0.13.9](https://togithub.com/dotnet/BenchmarkDotNet/issues?q=milestone:v0.13.9) scope, 3 issues were resolved and 7 pull requests were merged. This release includes 26 commits by 5 contributors. #### Resolved issues (3) - [#​2054](https://togithub.com/dotnet/BenchmarkDotNet/issues/2054) Custom logging/visualization during the benchmark run (assignee: [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [#​2404](https://togithub.com/dotnet/BenchmarkDotNet/issues/2404) Using `DisassemblyDiagnoser` in GitHub Actions (assignee: [@​timcassell](https://togithub.com/timcassell)) - [#​2432](https://togithub.com/dotnet/BenchmarkDotNet/issues/2432) Something went wrong with outliers when using `--launchCount` (assignee: [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Merged pull requests (7) - [#​1882](https://togithub.com/dotnet/BenchmarkDotNet/pull/1882) use coalesce instead of join (by [@​askazakov](https://togithub.com/askazakov)) - [#​2413](https://togithub.com/dotnet/BenchmarkDotNet/pull/2413) Fix linux crash from disassembler (by [@​timcassell](https://togithub.com/timcassell)) - [#​2420](https://togithub.com/dotnet/BenchmarkDotNet/pull/2420) Add event processor functionality (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [#​2421](https://togithub.com/dotnet/BenchmarkDotNet/pull/2421) More nullability warnings fixes (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [#​2433](https://togithub.com/dotnet/BenchmarkDotNet/pull/2433) Fix build errors with latest sdk (by [@​timcassell](https://togithub.com/timcassell)) - [#​2434](https://togithub.com/dotnet/BenchmarkDotNet/pull/2434) Fix Event Processors not being copied in ManualConfig.Add (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [#​2435](https://togithub.com/dotnet/BenchmarkDotNet/pull/2435) Treat warnings not as errors in manual test project (by [@​timcassell](https://togithub.com/timcassell)) #### Commits (26) - [ece5cc](https://togithub.com/dotnet/BenchmarkDotNet/commit/ece5ccfc91d92b610338b05da73d2a91508e2837) Set next BenchmarkDotNet version: 0.13.9 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [ad9376](https://togithub.com/dotnet/BenchmarkDotNet/commit/ad937654174e521741aac620e16635a8ff14b1c9) Add event processor functionality ([#​2420](https://togithub.com/dotnet/BenchmarkDotNet/issues/2420)) (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [8227bb](https://togithub.com/dotnet/BenchmarkDotNet/commit/8227bbfa5f4d22c51f9c3856576d3680d8fc0a92) Address PR feedback ([#​2434](https://togithub.com/dotnet/BenchmarkDotNet/issues/2434)) (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [46b3c0](https://togithub.com/dotnet/BenchmarkDotNet/commit/46b3c0171709c48f58966fdf2665b5f292ff6467) Fix linux crash from disassembler ([#​2413](https://togithub.com/dotnet/BenchmarkDotNet/issues/2413)) (by [@​timcassell](https://togithub.com/timcassell)) - [967a97](https://togithub.com/dotnet/BenchmarkDotNet/commit/967a975773ebd7a9744f3875220c7db8fa647957) Fix build errors with latest sdk. ([#​2433](https://togithub.com/dotnet/BenchmarkDotNet/issues/2433)) (by [@​timcassell](https://togithub.com/timcassell)) - [dd7a9b](https://togithub.com/dotnet/BenchmarkDotNet/commit/dd7a9b7cd132e522951eeb6916a3aa27a24ebf59) Treat warnings not as errors in manual test project ([#​2435](https://togithub.com/dotnet/BenchmarkDotNet/issues/2435)) (by [@​timcassell](https://togithub.com/timcassell)) - [583874](https://togithub.com/dotnet/BenchmarkDotNet/commit/58387457bd67c62fda9c831329401fe0de4ae86f) Print full stacktrace for GenerateException, see [#​2436](https://togithub.com/dotnet/BenchmarkDotNet/issues/2436) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [6e3a15](https://togithub.com/dotnet/BenchmarkDotNet/commit/6e3a159d3d3ae0d7eecc759c23a7bb0124e673df) Support WSL detection in RuntimeInformation (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8986e0](https://togithub.com/dotnet/BenchmarkDotNet/commit/8986e053c2fbc0befdef7d6e1a116a7bc83da282) Update myget url in README (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [516bd6](https://togithub.com/dotnet/BenchmarkDotNet/commit/516bd68238c38bb6f622f71039d7b91f3f33776d) Enabled nullability for BenchmarkDotNet.Diagnostics.dotTrace.csproj (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [5428eb](https://togithub.com/dotnet/BenchmarkDotNet/commit/5428ebdb8b6e9a847bb8ae6cf129b7dd9d784454) Fixed nullability warnings in methods signatures (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [7fbbc9](https://togithub.com/dotnet/BenchmarkDotNet/commit/7fbbc9f506cee0048f2ea6e7af15fbe1aa0bd7f7) Removed CanBeNull attribute (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [9d7350](https://togithub.com/dotnet/BenchmarkDotNet/commit/9d7350c21b30c2655705ede68929243846b8a407) Fixed warnings on null assignments (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [b43d28](https://togithub.com/dotnet/BenchmarkDotNet/commit/b43d280f1673526dff865f5fbfc1848c846eacdd) Fixed warnings in EngineEventLogParser (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [148165](https://togithub.com/dotnet/BenchmarkDotNet/commit/148165baf92233a3e3e67efc552e7528edb2fc78) Removed an unnecessary check (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [465aaf](https://togithub.com/dotnet/BenchmarkDotNet/commit/465aaf196a43d21b516edf6e9028c672c39937b9) Fixed empty catch warning (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [9a7bb7](https://togithub.com/dotnet/BenchmarkDotNet/commit/9a7bb7d5d6c72a01f991d869b9106364c26b1fce) \[build] Bump: Microsoft.DocAsCode.App 2.67.5 -> Docfx.App 2.71.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [9dd7b6](https://togithub.com/dotnet/BenchmarkDotNet/commit/9dd7b6f4b2511bbd30ba0f6d4999f7f58cf161a6) Fix license badge link in README (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [134b8e](https://togithub.com/dotnet/BenchmarkDotNet/commit/134b8edd09ad7dad0a17728eae4e9f50e16d3fe0) \[build] Automatic NextVersion evaluation (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8090d9](https://togithub.com/dotnet/BenchmarkDotNet/commit/8090d995e847c3c3d84db1fd5acbee312a75cf81) Suppress NETSDK1138 (TFM out of support warning) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [af610e](https://togithub.com/dotnet/BenchmarkDotNet/commit/af610eec251bfa74f7317eaec915df9b905c979b) Bump .NET SDK: 7.0.305->7.0.401 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8838ed](https://togithub.com/dotnet/BenchmarkDotNet/commit/8838ed4bf74377642d32774c558c0955e67c0faf) \[build] Fix docfx build warnings (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [2d379b](https://togithub.com/dotnet/BenchmarkDotNet/commit/2d379b37310983dbe645a2129066d9af65d9e0d7) Remove outlier consistency check, fix [#​2432](https://togithub.com/dotnet/BenchmarkDotNet/issues/2432) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [00628a](https://togithub.com/dotnet/BenchmarkDotNet/commit/00628ab31b79a78e1c22c298ca0086bdf28065a7) use coalesce instead of join (by [@​askazakov](https://togithub.com/askazakov)) - [411a6e](https://togithub.com/dotnet/BenchmarkDotNet/commit/411a6e7594c45c9ffa55b0b6caecb7f6ed1b3081) Prepare v0.13.9 changelog (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [228a46](https://togithub.com/dotnet/BenchmarkDotNet/commit/228a464e8be6c580ad9408e98f18813f6407fb5a) Rollback docfx.json (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Contributors (5) - Alina Smirnova ([@​alinasmirnova](https://togithub.com/alinasmirnova)) - Andrey Akinshin ([@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - askazakov ([@​askazakov](https://togithub.com/askazakov)) - Cameron Aavik ([@​caaavik-msft](https://togithub.com/caaavik-msft)) - Tim Cassell ([@​timcassell](https://togithub.com/timcassell)) Thank you very much! ### [`v0.13.8`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.8): 0.13.8 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.8.html #### Highlights This release contains important bug fixes. #### What's Changed - Issue2394 multiple markdown exporters not possible even with different names by [@​bstordrup](https://togithub.com/bstordrup) in [https://github.com/dotnet/BenchmarkDotNet/pull/2395](https://togithub.com/dotnet/BenchmarkDotNet/pull/2395) - Make MarkdownExporter ctor and Dialect protected by [@​nietras](https://togithub.com/nietras) in [https://github.com/dotnet/BenchmarkDotNet/pull/2407](https://togithub.com/dotnet/BenchmarkDotNet/pull/2407) - Refactor out base TextLogger from StreamLogger by [@​nietras](https://togithub.com/nietras) in [https://github.com/dotnet/BenchmarkDotNet/pull/2406](https://togithub.com/dotnet/BenchmarkDotNet/pull/2406) - - update the templates install command to reflect dotnet cli updates by [@​baywet](https://togithub.com/baywet) in [https://github.com/dotnet/BenchmarkDotNet/pull/2415](https://togithub.com/dotnet/BenchmarkDotNet/pull/2415) - Update stub decoding for .NET 8 for disassemblers by [@​janvorli](https://togithub.com/janvorli) in [https://github.com/dotnet/BenchmarkDotNet/pull/2416](https://togithub.com/dotnet/BenchmarkDotNet/pull/2416) - Enable nullability for BenchmarkDotNet.Annotations by [@​alinasmirnova](https://togithub.com/alinasmirnova) in [https://github.com/dotnet/BenchmarkDotNet/pull/2418](https://togithub.com/dotnet/BenchmarkDotNet/pull/2418) - Nullability In BenchmarkDotNet project by [@​alinasmirnova](https://togithub.com/alinasmirnova) in [https://github.com/dotnet/BenchmarkDotNet/pull/2419](https://togithub.com/dotnet/BenchmarkDotNet/pull/2419) - feat: add text justification style by [@​Vahdanian](https://togithub.com/Vahdanian) in [https://github.com/dotnet/BenchmarkDotNet/pull/2410](https://togithub.com/dotnet/BenchmarkDotNet/pull/2410) - Default to RoslynToolchain by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2409](https://togithub.com/dotnet/BenchmarkDotNet/pull/2409) #### New Contributors - [@​bstordrup](https://togithub.com/bstordrup) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2395](https://togithub.com/dotnet/BenchmarkDotNet/pull/2395) - [@​baywet](https://togithub.com/baywet) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2415](https://togithub.com/dotnet/BenchmarkDotNet/pull/2415) - [@​Vahdanian](https://togithub.com/Vahdanian) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2410](https://togithub.com/dotnet/BenchmarkDotNet/pull/2410) **Full Changelog**: https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.7...v0.13.8 ### [`v0.13.7`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.7): BenchmarkDotNet v0.13.7 This release contains a bunch of important bug fixes. Full changelog: https://benchmarkdotnet.org/changelog/v0.13.7.html #### What's Changed - Improve build for mono aot by [@​radical](https://togithub.com/radical) in [https://github.com/dotnet/BenchmarkDotNet/pull/2367](https://togithub.com/dotnet/BenchmarkDotNet/pull/2367) - IComparable fallback for Tuple/ValueTuple by [@​mrahhal](https://togithub.com/mrahhal) in [https://github.com/dotnet/BenchmarkDotNet/pull/2368](https://togithub.com/dotnet/BenchmarkDotNet/pull/2368) - Don't copy `PackageReference` in csproj by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2365](https://togithub.com/dotnet/BenchmarkDotNet/pull/2365) - Fix regression in parsing arguments with spaces Closes [#​2373](https://togithub.com/dotnet/BenchmarkDotNet/issues/2373) by [@​kant2002](https://togithub.com/kant2002) in [https://github.com/dotnet/BenchmarkDotNet/pull/2375](https://togithub.com/dotnet/BenchmarkDotNet/pull/2375) - `AggressiveOptimization` in `InProcess` toolchains by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2335](https://togithub.com/dotnet/BenchmarkDotNet/pull/2335) - Add expected results tests by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2361](https://togithub.com/dotnet/BenchmarkDotNet/pull/2361) - \[chore]: fix error message by [@​BurakTaner](https://togithub.com/BurakTaner) in [https://github.com/dotnet/BenchmarkDotNet/pull/2379](https://togithub.com/dotnet/BenchmarkDotNet/pull/2379) - Cancel old jobs on push by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2380](https://togithub.com/dotnet/BenchmarkDotNet/pull/2380) - Support `--cli` argument for `CsProjClassicNetToolchain` by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2381](https://togithub.com/dotnet/BenchmarkDotNet/pull/2381) - Rebuild .Net Framework projects by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2370](https://togithub.com/dotnet/BenchmarkDotNet/pull/2370) - Fix missing import on Debug build by [@​caaavik-msft](https://togithub.com/caaavik-msft) in [https://github.com/dotnet/BenchmarkDotNet/pull/2385](https://togithub.com/dotnet/BenchmarkDotNet/pull/2385) - perfcollect: don't restore symbols for local builds by [@​adamsitnik](https://togithub.com/adamsitnik) in [https://github.com/dotnet/BenchmarkDotNet/pull/2384](https://togithub.com/dotnet/BenchmarkDotNet/pull/2384) - Fix PlatformNotSupportedException thrown on Android in ConsoleTitler by [@​Adam--](https://togithub.com/Adam--) in [https://github.com/dotnet/BenchmarkDotNet/pull/2390](https://togithub.com/dotnet/BenchmarkDotNet/pull/2390) #### New Contributors - [@​BurakTaner](https://togithub.com/BurakTaner) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2379](https://togithub.com/dotnet/BenchmarkDotNet/pull/2379) - [@​caaavik-msft](https://togithub.com/caaavik-msft) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2385](https://togithub.com/dotnet/BenchmarkDotNet/pull/2385) - [@​Adam--](https://togithub.com/Adam--) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2390](https://togithub.com/dotnet/BenchmarkDotNet/pull/2390) **Full Changelog**: https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.6...v0.13.7 ### [`v0.13.6`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.6): BenchmarkDotNet v0.13.6 #### Highlights - New [BenchmarkDotNet.Diagnostics.dotTrace](https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace) NuGet package. Once this package is installed, you can annotate your benchmarks with the `[DotTraceDiagnoser]` and get a [dotTrace](https://www.jetbrains.com/profiler/) performance snapshot at the end of the benchmark run. [#​2328](https://togithub.com/dotnet/BenchmarkDotNet/pull/2328) - Updated documentation website. We migrated to [docfx](https://dotnet.github.io/docfx/) 2.67 and got the refreshed modern template based on bootstrap 5 with dark/light theme switcher. - Updated [BenchmarkDotNet.Templates](https://www.nuget.org/packages/BenchmarkDotNet.Templates). Multiple issues were resolved, now you can create new benchmark projects from terminal or your favorite IDE. [#​1658](https://togithub.com/dotnet/BenchmarkDotNet/issues/1658) [#​1881](https://togithub.com/dotnet/BenchmarkDotNet/issues/1881) [#​2149](https://togithub.com/dotnet/BenchmarkDotNet/issues/2149) [#​2338](https://togithub.com/dotnet/BenchmarkDotNet/pull/2338) - Response file support. Now it's possible to pass additional arguments to BenchmarkDotNet using `@filename` syntax. [#​2320](https://togithub.com/dotnet/BenchmarkDotNet/pull/2320) [#​2348](https://togithub.com/dotnet/BenchmarkDotNet/pull/2348) - Custom runtime support. [#​2285](https://togithub.com/dotnet/BenchmarkDotNet/pull/2285) - Introduce CategoryDiscoverer, see [`IntroCategoryDiscoverer`](xref:BenchmarkDotNet.Samples.IntroCategoryDiscoverer). [#​2306](https://togithub.com/dotnet/BenchmarkDotNet/issues/2306) [#​2307](https://togithub.com/dotnet/BenchmarkDotNet/pull/2307) - Multiple bug fixes. Full changelog: https://benchmarkdotnet.org/changelog/v0.13.6.html
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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://www.mend.io/free-developer-tools/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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b443a8af..f8a4159c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + From bb4f3526c2c2c2ca48ae61e883d6962847ebc5a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:03:17 -0400 Subject: [PATCH 195/316] chore(deps): update dependency dotnet-sdk to v8.0.400 (#295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dotnet-sdk](https://togithub.com/dotnet/sdk) | dotnet-sdk | patch | `8.0.303` -> `8.0.400` | --- ### Release Notes
dotnet/sdk (dotnet-sdk) ### [`v8.0.400`](https://togithub.com/dotnet/sdk/compare/v8.0.303...v8.0.400) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.304...v8.0.400) ### [`v8.0.304`](https://togithub.com/dotnet/sdk/compare/v8.0.303...v8.0.304) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.303...v8.0.304)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, 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://www.mend.io/free-developer-tools/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> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 9f8f3618..bc3a25e7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.303" + "version": "8.0.400" } } From 390205a41d29d786b5f41b0d91f34ec237276cb4 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 21 Aug 2024 11:36:05 -0400 Subject: [PATCH 196/316] chore: in-memory UpdateFlags to UpdateFlagsAsync (#298) I was doing an audit before the release and found this one method could use a suffix update. Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/InMemoryProvider.cs | 4 ++-- .../Providers/Memory/InMemoryProviderTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 771e2210..3283ea22 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -43,10 +43,10 @@ public InMemoryProvider(IDictionary? flags = null) } /// - /// Updating provider flags configuration, replacing all flags. + /// Update provider flag configuration, replacing all flags. /// /// the flags to use instead of the previous flags. - public async Task UpdateFlags(IDictionary? flags = null) + public async Task UpdateFlagsAsync(IDictionary? flags = null) { var changed = this._flags.Keys.ToList(); if (flags == null) diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 83974c23..c83ce0ce 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -170,7 +170,7 @@ public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( public async Task EmptyFlags_ShouldWork() { var provider = new InMemoryProvider(); - await provider.UpdateFlags(); + await provider.UpdateFlagsAsync(); Assert.Equal("InMemory", provider.GetMetadata().Name); } @@ -216,7 +216,7 @@ public async Task PutConfiguration_shouldUpdateConfigAndRunHandlers() Assert.True(details.Value); // update flags - await provider.UpdateFlags(new Dictionary(){ + await provider.UpdateFlagsAsync(new Dictionary(){ { "new-flag", new Flag( variants: new Dictionary(){ From 10af6cc6b3437f298c2182cf86ddfea2a565423b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:50:07 -0400 Subject: [PATCH 197/316] chore(main): release 2.0.0 (#254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.0.0](https://github.com/open-feature/dotnet-sdk/compare/v1.5.0...v2.0.0) (2024-08-21) Today we're announcing the release of the OpenFeature SDK for .NET, v2.0! This release contains several ergonomic improvements to the SDK, which .NET developers will appreciate. It also includes some performance optimizations brought to you by the latest .NET primitives. For details and migration tips, check out: https://openfeature.dev/blog/dotnet-sdk-v2 ### ⚠ BREAKING CHANGES * domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) * internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) * add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) * Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) * Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) ### πŸ› Bug Fixes * Add missing error message when an error occurred ([#256](https://github.com/open-feature/dotnet-sdk/issues/256)) ([949d53c](https://github.com/open-feature/dotnet-sdk/commit/949d53cada68bee8e80d113357fa6df8d425d3c1)) * Should map metadata when converting from ResolutionDetails to FlagEvaluationDetails ([#282](https://github.com/open-feature/dotnet-sdk/issues/282)) ([2f8bd21](https://github.com/open-feature/dotnet-sdk/commit/2f8bd2179ec35f79cbbab77206de78dd9b0f58d6)) ### ✨ New Features * add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) ([33154d2](https://github.com/open-feature/dotnet-sdk/commit/33154d2ed6b0b27f4a86a5fbad440a784a89c881)) * back targetingKey with internal map ([#287](https://github.com/open-feature/dotnet-sdk/issues/287)) ([ccc2f7f](https://github.com/open-feature/dotnet-sdk/commit/ccc2f7fbd4e4f67eb03c2e6a07140ca31225da2c)) * domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) ([4c0592e](https://github.com/open-feature/dotnet-sdk/commit/4c0592e6baf86d831fc7b39762c960ca0dd843a9)) * Drop net7 TFM ([#284](https://github.com/open-feature/dotnet-sdk/issues/284)) ([2dbe1f4](https://github.com/open-feature/dotnet-sdk/commit/2dbe1f4c95aeae501c8b5154b1ccefafa7df2632)) * internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) ([63faa84](https://github.com/open-feature/dotnet-sdk/commit/63faa8440cd650b0bd6c3ec009ad9bd78bc31f32)) * Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) ([ac7d7de](https://github.com/open-feature/dotnet-sdk/commit/ac7d7debf50cef08668bcd9457d3f830b8718806)) ### 🧹 Chore * cleanup code ([#277](https://github.com/open-feature/dotnet-sdk/issues/277)) ([44cf586](https://github.com/open-feature/dotnet-sdk/commit/44cf586f96607716fb8b4464d81edfd6074f7376)) * **deps:** Project file cleanup and remove unnecessary dependencies ([#251](https://github.com/open-feature/dotnet-sdk/issues/251)) ([79def47](https://github.com/open-feature/dotnet-sdk/commit/79def47106b19b316b691fa195f7160ddcfb9a41)) * **deps:** update actions/upload-artifact action to v4.3.3 ([#263](https://github.com/open-feature/dotnet-sdk/issues/263)) ([7718649](https://github.com/open-feature/dotnet-sdk/commit/77186495cd3d567b0aabd418f23a65567656b54d)) * **deps:** update actions/upload-artifact action to v4.3.4 ([#278](https://github.com/open-feature/dotnet-sdk/issues/278)) ([15189f1](https://github.com/open-feature/dotnet-sdk/commit/15189f1c6f7eb0931036e022eed68f58a1110b5b)) * **deps:** update actions/upload-artifact action to v4.3.5 ([#291](https://github.com/open-feature/dotnet-sdk/issues/291)) ([00e99d6](https://github.com/open-feature/dotnet-sdk/commit/00e99d6c2208b304748d00a931f460d6d6aab4de)) * **deps:** update codecov/codecov-action action to v4 ([#227](https://github.com/open-feature/dotnet-sdk/issues/227)) ([11a0333](https://github.com/open-feature/dotnet-sdk/commit/11a03332726f07dd0327d222e6bd6e1843db460c)) * **deps:** update codecov/codecov-action action to v4.3.1 ([#267](https://github.com/open-feature/dotnet-sdk/issues/267)) ([ff9df59](https://github.com/open-feature/dotnet-sdk/commit/ff9df593400f92c016eee1a45bd7097da008d4dc)) * **deps:** update codecov/codecov-action action to v4.5.0 ([#272](https://github.com/open-feature/dotnet-sdk/issues/272)) ([281295d](https://github.com/open-feature/dotnet-sdk/commit/281295d2999e4d36c5a2078cbfdfe5e59f4652b2)) * **deps:** update dependency benchmarkdotnet to v0.14.0 ([#293](https://github.com/open-feature/dotnet-sdk/issues/293)) ([aec222f](https://github.com/open-feature/dotnet-sdk/commit/aec222fe1b1a5b52f8349ceb98c12b636eb155eb)) * **deps:** update dependency coverlet.collector to v6.0.2 ([#247](https://github.com/open-feature/dotnet-sdk/issues/247)) ([ab34c16](https://github.com/open-feature/dotnet-sdk/commit/ab34c16b513ddbd0a53e925baaccd088163fbcc8)) * **deps:** update dependency coverlet.msbuild to v6.0.2 ([#239](https://github.com/open-feature/dotnet-sdk/issues/239)) ([e654222](https://github.com/open-feature/dotnet-sdk/commit/e6542222827cc25cd5a1acc5af47ce55149c0623)) * **deps:** update dependency dotnet-sdk to v8.0.204 ([#261](https://github.com/open-feature/dotnet-sdk/issues/261)) ([8f82645](https://github.com/open-feature/dotnet-sdk/commit/8f8264520814a42b7ed2af8f70340e7673259b6f)) * **deps:** update dependency dotnet-sdk to v8.0.301 ([#271](https://github.com/open-feature/dotnet-sdk/issues/271)) ([acd0385](https://github.com/open-feature/dotnet-sdk/commit/acd0385641e114a16d0ee56e3a143baa7d3c0535)) * **deps:** update dependency dotnet-sdk to v8.0.303 ([#275](https://github.com/open-feature/dotnet-sdk/issues/275)) ([871dcac](https://github.com/open-feature/dotnet-sdk/commit/871dcacc94fa2abb10434616c469cad6f674f07a)) * **deps:** update dependency dotnet-sdk to v8.0.400 ([#295](https://github.com/open-feature/dotnet-sdk/issues/295)) ([bb4f352](https://github.com/open-feature/dotnet-sdk/commit/bb4f3526c2c2c2ca48ae61e883d6962847ebc5a6)) * **deps:** update dependency githubactionstestlogger to v2.4.1 ([#274](https://github.com/open-feature/dotnet-sdk/issues/274)) ([46c2b15](https://github.com/open-feature/dotnet-sdk/commit/46c2b153c848bd3a500b828ddb89bd3b07753bf1)) * **deps:** update dependency microsoft.net.test.sdk to v17.10.0 ([#273](https://github.com/open-feature/dotnet-sdk/issues/273)) ([581ff81](https://github.com/open-feature/dotnet-sdk/commit/581ff81c7b1840c34840229bf20444c528c64cc6)) * **deps:** update dotnet monorepo ([#218](https://github.com/open-feature/dotnet-sdk/issues/218)) ([bc8301d](https://github.com/open-feature/dotnet-sdk/commit/bc8301d1c54e0b48ede3235877d969f28d61fb29)) * **deps:** update xunit-dotnet monorepo ([#262](https://github.com/open-feature/dotnet-sdk/issues/262)) ([43f14cc](https://github.com/open-feature/dotnet-sdk/commit/43f14cca072372ecacec89a949c85f763c1ee7b4)) * **deps:** update xunit-dotnet monorepo ([#279](https://github.com/open-feature/dotnet-sdk/issues/279)) ([fb1cc66](https://github.com/open-feature/dotnet-sdk/commit/fb1cc66440dd6bdbbef1ac1f85bf3228b80073af)) * **deps:** update xunit-dotnet monorepo to v2.8.1 ([#266](https://github.com/open-feature/dotnet-sdk/issues/266)) ([a7b6d85](https://github.com/open-feature/dotnet-sdk/commit/a7b6d8561716763f324325a8803b913c4d69c044)) * Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) ([5a5312c](https://github.com/open-feature/dotnet-sdk/commit/5a5312cc082ccd880b65165135e05b4f3b035df7)) * in-memory UpdateFlags to UpdateFlagsAsync ([#298](https://github.com/open-feature/dotnet-sdk/issues/298)) ([390205a](https://github.com/open-feature/dotnet-sdk/commit/390205a41d29d786b5f41b0d91f34ec237276cb4)) * prompt 2.0 ([9b9c3fd](https://github.com/open-feature/dotnet-sdk/commit/9b9c3fd09c27b191104d7ceaa726b6edd71fcd06)) * Support for determining spec support for the repo ([#270](https://github.com/open-feature/dotnet-sdk/issues/270)) ([67a1a0a](https://github.com/open-feature/dotnet-sdk/commit/67a1a0aea95ee943976990b1d1782e4061300b50)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Signed-off-by: Todd Baert Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Todd Baert --- .release-please-manifest.json | 2 +- CHANGELOG.md | 58 +++ README.md | 644 +++++++++++++++++----------------- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 383 insertions(+), 325 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index dd8fde77..895bf0e3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.0" + ".": "2.0.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 929d2c66..fb316f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Changelog +## [2.0.0](https://github.com/open-feature/dotnet-sdk/compare/v1.5.0...v2.0.0) (2024-08-21) + +Today we're announcing the release of the OpenFeature SDK for .NET, v2.0! This release contains several ergonomic improvements to the SDK, which .NET developers will appreciate. It also includes some performance optimizations brought to you by the latest .NET primitives. + +For details and migration tips, check out: https://openfeature.dev/blog/dotnet-sdk-v2 + +### ⚠ BREAKING CHANGES + +* domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) +* internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) +* add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) +* Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) +* Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) + +### πŸ› Bug Fixes + +* Add missing error message when an error occurred ([#256](https://github.com/open-feature/dotnet-sdk/issues/256)) ([949d53c](https://github.com/open-feature/dotnet-sdk/commit/949d53cada68bee8e80d113357fa6df8d425d3c1)) +* Should map metadata when converting from ResolutionDetails to FlagEvaluationDetails ([#282](https://github.com/open-feature/dotnet-sdk/issues/282)) ([2f8bd21](https://github.com/open-feature/dotnet-sdk/commit/2f8bd2179ec35f79cbbab77206de78dd9b0f58d6)) + + +### ✨ New Features + +* add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) ([33154d2](https://github.com/open-feature/dotnet-sdk/commit/33154d2ed6b0b27f4a86a5fbad440a784a89c881)) +* back targetingKey with internal map ([#287](https://github.com/open-feature/dotnet-sdk/issues/287)) ([ccc2f7f](https://github.com/open-feature/dotnet-sdk/commit/ccc2f7fbd4e4f67eb03c2e6a07140ca31225da2c)) +* domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) ([4c0592e](https://github.com/open-feature/dotnet-sdk/commit/4c0592e6baf86d831fc7b39762c960ca0dd843a9)) +* Drop net7 TFM ([#284](https://github.com/open-feature/dotnet-sdk/issues/284)) ([2dbe1f4](https://github.com/open-feature/dotnet-sdk/commit/2dbe1f4c95aeae501c8b5154b1ccefafa7df2632)) +* internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) ([63faa84](https://github.com/open-feature/dotnet-sdk/commit/63faa8440cd650b0bd6c3ec009ad9bd78bc31f32)) +* Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) ([ac7d7de](https://github.com/open-feature/dotnet-sdk/commit/ac7d7debf50cef08668bcd9457d3f830b8718806)) + + +### 🧹 Chore + +* cleanup code ([#277](https://github.com/open-feature/dotnet-sdk/issues/277)) ([44cf586](https://github.com/open-feature/dotnet-sdk/commit/44cf586f96607716fb8b4464d81edfd6074f7376)) +* **deps:** Project file cleanup and remove unnecessary dependencies ([#251](https://github.com/open-feature/dotnet-sdk/issues/251)) ([79def47](https://github.com/open-feature/dotnet-sdk/commit/79def47106b19b316b691fa195f7160ddcfb9a41)) +* **deps:** update actions/upload-artifact action to v4.3.3 ([#263](https://github.com/open-feature/dotnet-sdk/issues/263)) ([7718649](https://github.com/open-feature/dotnet-sdk/commit/77186495cd3d567b0aabd418f23a65567656b54d)) +* **deps:** update actions/upload-artifact action to v4.3.4 ([#278](https://github.com/open-feature/dotnet-sdk/issues/278)) ([15189f1](https://github.com/open-feature/dotnet-sdk/commit/15189f1c6f7eb0931036e022eed68f58a1110b5b)) +* **deps:** update actions/upload-artifact action to v4.3.5 ([#291](https://github.com/open-feature/dotnet-sdk/issues/291)) ([00e99d6](https://github.com/open-feature/dotnet-sdk/commit/00e99d6c2208b304748d00a931f460d6d6aab4de)) +* **deps:** update codecov/codecov-action action to v4 ([#227](https://github.com/open-feature/dotnet-sdk/issues/227)) ([11a0333](https://github.com/open-feature/dotnet-sdk/commit/11a03332726f07dd0327d222e6bd6e1843db460c)) +* **deps:** update codecov/codecov-action action to v4.3.1 ([#267](https://github.com/open-feature/dotnet-sdk/issues/267)) ([ff9df59](https://github.com/open-feature/dotnet-sdk/commit/ff9df593400f92c016eee1a45bd7097da008d4dc)) +* **deps:** update codecov/codecov-action action to v4.5.0 ([#272](https://github.com/open-feature/dotnet-sdk/issues/272)) ([281295d](https://github.com/open-feature/dotnet-sdk/commit/281295d2999e4d36c5a2078cbfdfe5e59f4652b2)) +* **deps:** update dependency benchmarkdotnet to v0.14.0 ([#293](https://github.com/open-feature/dotnet-sdk/issues/293)) ([aec222f](https://github.com/open-feature/dotnet-sdk/commit/aec222fe1b1a5b52f8349ceb98c12b636eb155eb)) +* **deps:** update dependency coverlet.collector to v6.0.2 ([#247](https://github.com/open-feature/dotnet-sdk/issues/247)) ([ab34c16](https://github.com/open-feature/dotnet-sdk/commit/ab34c16b513ddbd0a53e925baaccd088163fbcc8)) +* **deps:** update dependency coverlet.msbuild to v6.0.2 ([#239](https://github.com/open-feature/dotnet-sdk/issues/239)) ([e654222](https://github.com/open-feature/dotnet-sdk/commit/e6542222827cc25cd5a1acc5af47ce55149c0623)) +* **deps:** update dependency dotnet-sdk to v8.0.204 ([#261](https://github.com/open-feature/dotnet-sdk/issues/261)) ([8f82645](https://github.com/open-feature/dotnet-sdk/commit/8f8264520814a42b7ed2af8f70340e7673259b6f)) +* **deps:** update dependency dotnet-sdk to v8.0.301 ([#271](https://github.com/open-feature/dotnet-sdk/issues/271)) ([acd0385](https://github.com/open-feature/dotnet-sdk/commit/acd0385641e114a16d0ee56e3a143baa7d3c0535)) +* **deps:** update dependency dotnet-sdk to v8.0.303 ([#275](https://github.com/open-feature/dotnet-sdk/issues/275)) ([871dcac](https://github.com/open-feature/dotnet-sdk/commit/871dcacc94fa2abb10434616c469cad6f674f07a)) +* **deps:** update dependency dotnet-sdk to v8.0.400 ([#295](https://github.com/open-feature/dotnet-sdk/issues/295)) ([bb4f352](https://github.com/open-feature/dotnet-sdk/commit/bb4f3526c2c2c2ca48ae61e883d6962847ebc5a6)) +* **deps:** update dependency githubactionstestlogger to v2.4.1 ([#274](https://github.com/open-feature/dotnet-sdk/issues/274)) ([46c2b15](https://github.com/open-feature/dotnet-sdk/commit/46c2b153c848bd3a500b828ddb89bd3b07753bf1)) +* **deps:** update dependency microsoft.net.test.sdk to v17.10.0 ([#273](https://github.com/open-feature/dotnet-sdk/issues/273)) ([581ff81](https://github.com/open-feature/dotnet-sdk/commit/581ff81c7b1840c34840229bf20444c528c64cc6)) +* **deps:** update dotnet monorepo ([#218](https://github.com/open-feature/dotnet-sdk/issues/218)) ([bc8301d](https://github.com/open-feature/dotnet-sdk/commit/bc8301d1c54e0b48ede3235877d969f28d61fb29)) +* **deps:** update xunit-dotnet monorepo ([#262](https://github.com/open-feature/dotnet-sdk/issues/262)) ([43f14cc](https://github.com/open-feature/dotnet-sdk/commit/43f14cca072372ecacec89a949c85f763c1ee7b4)) +* **deps:** update xunit-dotnet monorepo ([#279](https://github.com/open-feature/dotnet-sdk/issues/279)) ([fb1cc66](https://github.com/open-feature/dotnet-sdk/commit/fb1cc66440dd6bdbbef1ac1f85bf3228b80073af)) +* **deps:** update xunit-dotnet monorepo to v2.8.1 ([#266](https://github.com/open-feature/dotnet-sdk/issues/266)) ([a7b6d85](https://github.com/open-feature/dotnet-sdk/commit/a7b6d8561716763f324325a8803b913c4d69c044)) +* Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) ([5a5312c](https://github.com/open-feature/dotnet-sdk/commit/5a5312cc082ccd880b65165135e05b4f3b035df7)) +* in-memory UpdateFlags to UpdateFlagsAsync ([#298](https://github.com/open-feature/dotnet-sdk/issues/298)) ([390205a](https://github.com/open-feature/dotnet-sdk/commit/390205a41d29d786b5f41b0d91f34ec237276cb4)) +* prompt 2.0 ([9b9c3fd](https://github.com/open-feature/dotnet-sdk/commit/9b9c3fd09c27b191104d7ceaa726b6edd71fcd06)) +* Support for determining spec support for the repo ([#270](https://github.com/open-feature/dotnet-sdk/issues/270)) ([67a1a0a](https://github.com/open-feature/dotnet-sdk/commit/67a1a0aea95ee943976990b1d1782e4061300b50)) + ## [1.5.0](https://github.com/open-feature/dotnet-sdk/compare/v1.4.1...v1.5.0) (2024-03-12) diff --git a/README.md b/README.md index bce047f8..b8f25012 100644 --- a/README.md +++ b/README.md @@ -1,322 +1,322 @@ - - - -![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) - -## .NET SDK - - - -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) -[ - ![Release](https://img.shields.io/static/v1?label=release&message=v1.5.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.5.0) - -[![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -[![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) -[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) - - -[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. - - - -## πŸš€ Quick start - -### Requirements - -- .NET 6+ -- .NET Core 6+ -- .NET Framework 4.6.2+ - -Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 - -### Install - -Use the following to initialize your project: - -```sh -dotnet new console -``` - -and install OpenFeature: - -```sh -dotnet add package OpenFeature -``` - -### Usage - -```csharp -public async Task Example() -{ - // Register your feature flag provider - await Api.Instance.SetProviderAsync(new InMemoryProvider()); - - // Create a new client - FeatureClient client = Api.Instance.GetClient(); - - // Evaluate your feature flag - bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false); - - if ( v2Enabled ) - { - //Do some work - } -} -``` - -## 🌟 Features - -| Status | Features | Description | -| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| βœ… | [Logging](#logging) | Integrate with popular logging packages. | -| βœ… | [Domains](#domains) | Logically bind clients with providers. | -| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | - -> Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ - -### Providers - -[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. -Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). - -If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. - -Once you've added a provider as a dependency, it can be registered with OpenFeature like this: - -```csharp -await Api.Instance.SetProviderAsync(new MyProvider()); -``` - -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. - -### Targeting - -Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. -In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). -If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). - -```csharp -// set a value to the global context -EvaluationContextBuilder builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext apiCtx = builder.Build(); -Api.Instance.SetContext(apiCtx); - -// set a value to the client context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext clientCtx = builder.Build(); -var client = Api.Instance.GetClient(); -client.SetContext(clientCtx); - -// set a value to the invocation context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext reqCtx = builder.Build(); - -bool flagValue = await client.GetBooleanValuAsync("some-flag", false, reqCtx); - -``` - -### Hooks - -[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. -If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. - -Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. - -```csharp -// add a hook globally, to run on all evaluations -Api.Instance.AddHooks(new ExampleGlobalHook()); - -// add a hook on this client, to run on all evaluations made by this client -var client = Api.Instance.GetClient(); -client.AddHooks(new ExampleClientHook()); - -// add a hook for this evaluation only -var value = await client.GetBooleanValueAsync("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); -``` - -### Logging - -The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. - -### Domains - -Clients can be assigned to a domain. -A domain is a logical identifier which can be used to associate clients with a particular provider. -If a domain has no associated provider, the default provider is used. - -```csharp -// registering the default provider -await Api.Instance.SetProviderAsync(new LocalProvider()); - -// registering a provider to a domain -await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); - -// a client backed by default provider -FeatureClient clientDefault = Api.Instance.GetClient(); - -// a client backed by CachedProvider -FeatureClient scopedClient = Api.Instance.GetClient("clientForCache"); -``` - -Domains can be defined on a provider during registration. -For more details, please refer to the [providers](#providers) section. - -### Eventing - -Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, -provider readiness, or error conditions. -Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. -Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. - -Please refer to the documentation of the provider you're using to see what events are supported. - -Example usage of an Event handler: - -```csharp -public static void EventHandler(ProviderEventPayload eventDetails) -{ - Console.WriteLine(eventDetails.Type); -} -``` - -```csharp -EventHandlerDelegate callback = EventHandler; -// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event -Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -It is also possible to register an event handler for a specific client, as in the following example: - -```csharp -EventHandlerDelegate callback = EventHandler; - -var myClient = Api.Instance.GetClient("my-client"); - -var provider = new ExampleProvider(); -await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider); - -myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -### Shutdown - -The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. - -```csharp -// Shut down all providers -await Api.Instance.ShutdownAsync(); -``` - -## Extending - -### Develop a provider - -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. - -```csharp -public class MyProvider : FeatureProvider -{ - public override Metadata GetMetadata() - { - return new Metadata("My Provider"); - } - - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve a boolean flag value - } - - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve a string flag value - } - - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) - { - // resolve an int flag value - } - - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve a double flag value - } - - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve an object flag value - } -} -``` - -### Develop a hook - -To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -Implement your own hook by conforming to the `Hook interface`. -To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. - -```csharp -public class MyHook : Hook -{ - public ValueTask BeforeAsync(HookContext context, - IReadOnlyDictionary hints = null) - { - // code to run before flag evaluation - } - - public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) - { - // code to run after successful flag evaluation - } - - public ValueTask ErrorAsync(HookContext context, Exception error, - IReadOnlyDictionary hints = null) - { - // code to run if there's an error during before hooks or during flag evaluation - } - - public ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null) - { - // code to run after all other stages, regardless of success/failure - } -} -``` - -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! - - -## ⭐️ Support the project - -- Give this repo a ⭐️! -- Follow us on social media: - - Twitter: [@openfeature](https://twitter.com/openfeature) - - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) -- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more information, check out our [community page](https://openfeature.dev/community/) - -## 🀝 Contributing - -Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. - -### Thanks to everyone who has already contributed - -[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) - -Made with [contrib.rocks](https://contrib.rocks). - + + + +![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) + +## .NET SDK + + + +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) +[ + ![Release](https://img.shields.io/static/v1?label=release&message=v2.0.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.0.0) + +[![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) +[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) + + +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. + + + +## πŸš€ Quick start + +### Requirements + +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ + +Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 + +### Install + +Use the following to initialize your project: + +```sh +dotnet new console +``` + +and install OpenFeature: + +```sh +dotnet add package OpenFeature +``` + +### Usage + +```csharp +public async Task Example() +{ + // Register your feature flag provider + await Api.Instance.SetProviderAsync(new InMemoryProvider()); + + // Create a new client + FeatureClient client = Api.Instance.GetClient(); + + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false); + + if ( v2Enabled ) + { + //Do some work + } +} +``` + +## 🌟 Features + +| Status | Features | Description | +| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Logging](#logging) | Integrate with popular logging packages. | +| βœ… | [Domains](#domains) | Logically bind clients with providers. | +| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | + +> Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ + +### Providers + +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). + +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. + +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +```csharp +await Api.Instance.SetProviderAsync(new MyProvider()); +``` + +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. + +### Targeting + +Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). + +```csharp +// set a value to the global context +EvaluationContextBuilder builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext apiCtx = builder.Build(); +Api.Instance.SetContext(apiCtx); + +// set a value to the client context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext clientCtx = builder.Build(); +var client = Api.Instance.GetClient(); +client.SetContext(clientCtx); + +// set a value to the invocation context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext reqCtx = builder.Build(); + +bool flagValue = await client.GetBooleanValuAsync("some-flag", false, reqCtx); + +``` + +### Hooks + +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. + +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. + +```csharp +// add a hook globally, to run on all evaluations +Api.Instance.AddHooks(new ExampleGlobalHook()); + +// add a hook on this client, to run on all evaluations made by this client +var client = Api.Instance.GetClient(); +client.AddHooks(new ExampleClientHook()); + +// add a hook for this evaluation only +var value = await client.GetBooleanValueAsync("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); +``` + +### Logging + +The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. + +### Domains + +Clients can be assigned to a domain. +A domain is a logical identifier which can be used to associate clients with a particular provider. +If a domain has no associated provider, the default provider is used. + +```csharp +// registering the default provider +await Api.Instance.SetProviderAsync(new LocalProvider()); + +// registering a provider to a domain +await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); + +// a client backed by default provider +FeatureClient clientDefault = Api.Instance.GetClient(); + +// a client backed by CachedProvider +FeatureClient scopedClient = Api.Instance.GetClient("clientForCache"); +``` + +Domains can be defined on a provider during registration. +For more details, please refer to the [providers](#providers) section. + +### Eventing + +Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, +provider readiness, or error conditions. +Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. +Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. + +Please refer to the documentation of the provider you're using to see what events are supported. + +Example usage of an Event handler: + +```csharp +public static void EventHandler(ProviderEventPayload eventDetails) +{ + Console.WriteLine(eventDetails.Type); +} +``` + +```csharp +EventHandlerDelegate callback = EventHandler; +// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event +Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +It is also possible to register an event handler for a specific client, as in the following example: + +```csharp +EventHandlerDelegate callback = EventHandler; + +var myClient = Api.Instance.GetClient("my-client"); + +var provider = new ExampleProvider(); +await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider); + +myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +### Shutdown + +The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. + +```csharp +// Shut down all providers +await Api.Instance.ShutdownAsync(); +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +```csharp +public class MyProvider : FeatureProvider +{ + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a boolean flag value + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a string flag value + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a double flag value + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve an object flag value + } +} +``` + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. + +```csharp +public class MyHook : Hook +{ + public ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary hints = null) + { + // code to run before flag evaluation + } + + public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null) + { + // code to run after successful flag evaluation + } + + public ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary hints = null) + { + // code to run if there's an error during before hooks or during flag evaluation + } + + public ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null) + { + // code to run after all other stages, regardless of success/failure + } +} +``` + +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! + + +## ⭐️ Support the project + +- Give this repo a ⭐️! +- Follow us on social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more information, check out our [community page](https://openfeature.dev/community/) + +## 🀝 Contributing + +Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. + +### Thanks to everyone who has already contributed + +[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) + +Made with [contrib.rocks](https://contrib.rocks). + diff --git a/build/Common.prod.props b/build/Common.prod.props index 2431a810..656f3476 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 1.5.0 + 2.0.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index bc80560f..227cea21 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.5.0 +2.0.0 From 5593e19ca990196f754cd0be69391abb8f0dbcd5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 08:25:45 +1000 Subject: [PATCH 198/316] chore(deps): update dependency microsoft.net.test.sdk to 17.11.0 (#297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Microsoft.NET.Test.Sdk](https://togithub.com/microsoft/vstest) | `17.10.0` -> `17.11.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.NET.Test.Sdk/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.NET.Test.Sdk/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.NET.Test.Sdk/17.10.0/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.NET.Test.Sdk/17.10.0/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
microsoft/vstest (Microsoft.NET.Test.Sdk) ### [`v17.11.0`](https://togithub.com/microsoft/vstest/releases/tag/v17.11.0) #### What's Changed - Add reference to the AdapterUtilities library in the spec docs. by [@​peterwald](https://togithub.com/peterwald) in [https://github.com/microsoft/vstest/pull/4958](https://togithub.com/microsoft/vstest/pull/4958) - Stack trace when localized, and new messages by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4944](https://togithub.com/microsoft/vstest/pull/4944) - Fix single quote and space in F# pretty methods by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4969](https://togithub.com/microsoft/vstest/pull/4969) - Update .NET runtimes to latest patch version by [@​Evangelink](https://togithub.com/Evangelink) in [https://github.com/microsoft/vstest/pull/4975](https://togithub.com/microsoft/vstest/pull/4975) - Update dotnetcoretests.md by [@​DickBaker](https://togithub.com/DickBaker) in [https://github.com/microsoft/vstest/pull/4977](https://togithub.com/microsoft/vstest/pull/4977) - Add list of known TestingPlatform dlls by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4983](https://togithub.com/microsoft/vstest/pull/4983) - Update framework version used for testing, and test matrix by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4970](https://togithub.com/microsoft/vstest/pull/4970) - Add output forwarding for .NET by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4988](https://togithub.com/microsoft/vstest/pull/4988) - Remove usage of pt images before decomissioning by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4994](https://togithub.com/microsoft/vstest/pull/4994) - chore: Add more details to acquistion section. by [@​voroninp](https://togithub.com/voroninp) in [https://github.com/microsoft/vstest/pull/4999](https://togithub.com/microsoft/vstest/pull/4999) - Simplify banner by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5013](https://togithub.com/microsoft/vstest/pull/5013) - Forward standard output of testhost by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4998](https://togithub.com/microsoft/vstest/pull/4998) - Add missing copyright header by [@​MichaelSimons](https://togithub.com/MichaelSimons) in [https://github.com/microsoft/vstest/pull/5020](https://togithub.com/microsoft/vstest/pull/5020) - Add option to not share .NET Framework testhosts by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5018](https://togithub.com/microsoft/vstest/pull/5018) - GetTypesToLoad Attribute cant be null by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5054](https://togithub.com/microsoft/vstest/pull/5054) - rawArgument in GetArgumentList cant be null by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5056](https://togithub.com/microsoft/vstest/pull/5056) - fix Atribute typo by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5057](https://togithub.com/microsoft/vstest/pull/5057) - remove unnecessary list alloc for 2 scenarios in TestRequestManager.GetSources by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5058](https://togithub.com/microsoft/vstest/pull/5058) - fix incompatiblity typo by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5059](https://togithub.com/microsoft/vstest/pull/5059) - remove redundant inline method in IsPlatformIncompatible by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5060](https://togithub.com/microsoft/vstest/pull/5060) - fix Sucess typo by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5061](https://togithub.com/microsoft/vstest/pull/5061) - use some null coalescing by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5062](https://togithub.com/microsoft/vstest/pull/5062) - Add cts into friends of TranslationLayer by [@​jakubch1](https://togithub.com/jakubch1) in [https://github.com/microsoft/vstest/pull/5075](https://togithub.com/microsoft/vstest/pull/5075) - Use built in sha1 for id generation by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5081](https://togithub.com/microsoft/vstest/pull/5081) - All output in terminal logger by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5083](https://togithub.com/microsoft/vstest/pull/5083) - Ignore env test by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5095](https://togithub.com/microsoft/vstest/pull/5095) - Dispose XmlReader in XmlRunSettingsUtilities by [@​omajid](https://togithub.com/omajid) in [https://github.com/microsoft/vstest/pull/5094](https://togithub.com/microsoft/vstest/pull/5094) - Bump to macos-12 build image by [@​akoeplinger](https://togithub.com/akoeplinger) in [https://github.com/microsoft/vstest/pull/5101](https://togithub.com/microsoft/vstest/pull/5101) - Handle ansi escape in terminal logger reporter by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5084](https://togithub.com/microsoft/vstest/pull/5084) - remove disable interactive auth by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5110](https://togithub.com/microsoft/vstest/pull/5110) - Error output as info in terminal logger by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5113](https://togithub.com/microsoft/vstest/pull/5113) - Write dll instead of target on abort, rename errors by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5115](https://togithub.com/microsoft/vstest/pull/5115) - - \[rel/17.11] Update dependencies from devdiv/DevDiv/vs-code-coverage by [@​dotnet-maestro](https://togithub.com/dotnet-maestro) in [https://github.com/microsoft/vstest/pull/5152](https://togithub.com/microsoft/vstest/pull/5152) #### New Contributors - [@​peterwald](https://togithub.com/peterwald) made their first contribution in [https://github.com/microsoft/vstest/pull/4958](https://togithub.com/microsoft/vstest/pull/4958) - [@​DickBaker](https://togithub.com/DickBaker) made their first contribution in [https://github.com/microsoft/vstest/pull/4977](https://togithub.com/microsoft/vstest/pull/4977) - [@​voroninp](https://togithub.com/voroninp) made their first contribution in [https://github.com/microsoft/vstest/pull/4999](https://togithub.com/microsoft/vstest/pull/4999) - [@​akoeplinger](https://togithub.com/akoeplinger) made their first contribution in [https://github.com/microsoft/vstest/pull/5101](https://togithub.com/microsoft/vstest/pull/5101) **Full Changelog**: https://github.com/microsoft/vstest/compare/v17.10.0...v17.11.0-release-24352-06
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f8a4159c..7227000a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From 0bae29d4771c4901e0c511b8d3587e6501e67ecd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 06:50:02 +0100 Subject: [PATCH 199/316] chore(deps): update dependency dotnet-sdk to v8.0.401 (#296) --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index bc3a25e7..caa8a0e4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.400" + "version": "8.0.401" } } From c471c062cf70d78b67f597f468c62dbfbf0674d2 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 24 Sep 2024 16:17:35 -0400 Subject: [PATCH 200/316] chore: update release please config (#304) ## This PR - removes the release as configuration - removes pre-1.0 configurations Signed-off-by: Michael Beemer --- release-please-config.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index 4ccbcc43..e79a24e5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,12 +1,9 @@ { "packages": { ".": { - "release-as": "2.0.0", "release-type": "simple", "monorepo-tags": false, "include-component-in-tag": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, "versioning": "default", "extra-files": [ "build/Common.prod.props", From 9b693f737f111ed878749f725dd4c831206b308a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:59:14 +0000 Subject: [PATCH 201/316] chore(deps): update actions/upload-artifact action to v4.4.3 (#292) 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 | |---|---|---|---| | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | minor | `v4.3.5` -> `v4.4.3` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.4.3`](https://redirect.github.com/actions/upload-artifact/releases/tag/v4.4.3) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.4.2...v4.4.3) ##### What's Changed - Undo indirect dependency updates from [#​627](https://redirect.github.com/actions/upload-artifact/issues/627) by [@​joshmgross](https://redirect.github.com/joshmgross) in [https://github.com/actions/upload-artifact/pull/632](https://redirect.github.com/actions/upload-artifact/pull/632) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4.4.2...v4.4.3 ### [`v4.4.2`](https://redirect.github.com/actions/upload-artifact/releases/tag/v4.4.2) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.4.1...v4.4.2) ##### What's Changed - Bump `@actions/artifact` to 2.1.11 by [@​robherley](https://redirect.github.com/robherley) in [https://github.com/actions/upload-artifact/pull/627](https://redirect.github.com/actions/upload-artifact/pull/627) - Includes fix for relative symlinks not resolving properly **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4.4.1...v4.4.2 ### [`v4.4.1`](https://redirect.github.com/actions/upload-artifact/releases/tag/v4.4.1) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.4.0...v4.4.1) ##### What's Changed - Add a section about hidden files by [@​joshmgross](https://redirect.github.com/joshmgross) in [https://github.com/actions/upload-artifact/pull/607](https://redirect.github.com/actions/upload-artifact/pull/607) - Add workflow file for publishing releases to immutable action package by [@​Jcambass](https://redirect.github.com/Jcambass) in [https://github.com/actions/upload-artifact/pull/621](https://redirect.github.com/actions/upload-artifact/pull/621) - Update [@​actions/artifact](https://redirect.github.com/actions/artifact) to latest version, includes symlink and timeout fixes by [@​robherley](https://redirect.github.com/robherley) in [https://github.com/actions/upload-artifact/pull/625](https://redirect.github.com/actions/upload-artifact/pull/625) ##### New Contributors - [@​Jcambass](https://redirect.github.com/Jcambass) made their first contribution in [https://github.com/actions/upload-artifact/pull/621](https://redirect.github.com/actions/upload-artifact/pull/621) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4.4.0...v4.4.1 ### [`v4.4.0`](https://redirect.github.com/actions/upload-artifact/compare/v4.3.6...v4.4.0) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.3.6...v4.4.0) ### [`v4.3.6`](https://redirect.github.com/actions/upload-artifact/compare/v4.3.5...v4.3.6) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.3.5...v4.3.6)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13092d45..acd6d428 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.4.3 with: name: nupkgs path: src/**/*.nupkg From 30381423333c54e1df98d7721dd72697fc5406dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 11 Nov 2024 23:16:41 +0000 Subject: [PATCH 202/316] fix: Fix unit test clean context (#313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR - It fixes an issue where the context sometimes needs to be cleared correctly. - It should eliminate any further race conditions in the unit tests. ### Notes Following an investigation on xUnit shared contexts, I found we should use `IDisposable` to clear data between tests. See https://xunit.net/docs/shared-context for reference. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .../ClearOpenFeatureInstanceFixture.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs index 5f31c71a..5a620214 100644 --- a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs +++ b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs @@ -1,13 +1,14 @@ -namespace OpenFeature.Tests +using System; + +namespace OpenFeature.Tests; + +public class ClearOpenFeatureInstanceFixture : IDisposable { - public class ClearOpenFeatureInstanceFixture + // Make sure the singleton is cleared between tests + public void Dispose() { - // Make sure the singleton is cleared between tests - public ClearOpenFeatureInstanceFixture() - { - Api.Instance.SetContext(null); - Api.Instance.ClearHooks(); - Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).Wait(); - } + Api.Instance.SetContext(null); + Api.Instance.ClearHooks(); + Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).Wait(); } } From 5b979d290d96020ffe7f3e5729550d6f988b2af2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:34:34 +0000 Subject: [PATCH 203/316] chore(deps): update dependency microsoft.net.test.sdk to 17.11.1 (#301) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7227000a..e5dbb2ad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From bc7e187b7586a04e0feb9ef28291ce14c9ac35c5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:43:13 +0000 Subject: [PATCH 204/316] chore(deps): update dependency fluentassertions to 6.12.2 (#302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [FluentAssertions](https://www.fluentassertions.com/) ([source](https://redirect.github.com/fluentassertions/fluentassertions)) | `6.12.0` -> `6.12.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/FluentAssertions/6.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/FluentAssertions/6.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/FluentAssertions/6.12.0/6.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/FluentAssertions/6.12.0/6.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
fluentassertions/fluentassertions (FluentAssertions) ### [`v6.12.2`](https://redirect.github.com/fluentassertions/fluentassertions/releases/tag/6.12.2) [Compare Source](https://redirect.github.com/fluentassertions/fluentassertions/compare/6.12.1...6.12.2) #### What's Changed ##### Others - Better support for default interface and explicitly implemented properties by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2794](https://redirect.github.com/fluentassertions/fluentassertions/pull/2794) **Full Changelog**: https://github.com/fluentassertions/fluentassertions/compare/6.12.1...6.12.2 ### [`v6.12.1`](https://redirect.github.com/fluentassertions/fluentassertions/releases/tag/6.12.1) [Compare Source](https://redirect.github.com/fluentassertions/fluentassertions/compare/6.12.0...6.12.1) ##### What's Changed ##### Improvements - Improve `BeEmpty()` and `BeNullOrEmpty()` performance for `IEnumerable`, by materializing only the first item - [#​2530](https://redirect.github.com/fluentassertions/fluentassertions/pull/2530) ##### Fixes - Fixed formatting error when checking nullable `DateTimeOffset` with `BeWithin(...).Before(...)` - [#​2312](https://redirect.github.com/fluentassertions/fluentassertions/pull/2312) - `BeEquivalentTo` will now find and can map subject properties that are implemented through an explicitly-implemented interface - [#​2152](https://redirect.github.com/fluentassertions/fluentassertions/pull/2152) - Fixed that the `because` and `becauseArgs` were not passed down the equivalency tree - [#​2318](https://redirect.github.com/fluentassertions/fluentassertions/pull/2318) - `BeEquivalentTo` can again compare a non-generic `IDictionary` with a generic one - [#​2358](https://redirect.github.com/fluentassertions/fluentassertions/pull/2358) - Fixed that the `FormattingOptions` were not respected in inner `AssertionScope` - [#​2329](https://redirect.github.com/fluentassertions/fluentassertions/pull/2329) - Capitalize `true` and `false` in failure messages and make them formattable to a custom `BooleanFormatter` - [#​2390](https://redirect.github.com/fluentassertions/fluentassertions/pull/2390), [#​2393](https://redirect.github.com/fluentassertions/fluentassertions/pull/2393) - Improved the failure message for `NotBeOfType` when wrapped in an `AssertionScope` and the subject is null - [#​2399](https://redirect.github.com/fluentassertions/fluentassertions/pull/2399) - Improved the failure message for `BeWritable`/`BeReadable` when wrapped in an `AssertionScope` and the subject is read-only/write-only - [#​2399](https://redirect.github.com/fluentassertions/fluentassertions/pull/2399) - Improved the failure message for `ThrowExactly[Async]` when wrapped in an `AssertionScope` and no exception is thrown - [#​2398](https://redirect.github.com/fluentassertions/fluentassertions/pull/2398) - Improved the failure message for `[Not]HaveExplicitProperty` when wrapped in an `AssertionScope` and not implementing the interface - [#​2403](https://redirect.github.com/fluentassertions/fluentassertions/pull/2403) - Improved the failure message for `[Not]HaveExplicitMethod` when wrapped in an `AssertionScope` and not implementing the interface - [#​2403](https://redirect.github.com/fluentassertions/fluentassertions/pull/2403) - Changed `BeEquivalentTo` to exclude `private protected` members from the comparison - [#​2417](https://redirect.github.com/fluentassertions/fluentassertions/pull/2417) - Fixed using `BeEquivalentTo` on an empty `ArraySegment` - [#​2445](https://redirect.github.com/fluentassertions/fluentassertions/pull/2445), [#​2511](https://redirect.github.com/fluentassertions/fluentassertions/pull/2511) - `BeEquivalentTo` with a custom comparer can now handle null values - [#​2489](https://redirect.github.com/fluentassertions/fluentassertions/pull/2489) - Ensured that nested calls to `AssertionScope(context)` create a chained context - [#​2607](https://redirect.github.com/fluentassertions/fluentassertions/pull/2607) - One overload of the `AssertionScope` constructor would not create an actual scope associated with the thread - [#​2607](https://redirect.github.com/fluentassertions/fluentassertions/pull/2607) - Fixed `ThrowWithinAsync` not respecting `OperationCanceledException` - [#​2614](https://redirect.github.com/fluentassertions/fluentassertions/pull/2614) - Fixed using `BeEquivalentTo` with an `IEqualityComparer` targeting nullable types - [#​2648](https://redirect.github.com/fluentassertions/fluentassertions/pull/2648) **Full Changelog**: https://github.com/fluentassertions/fluentassertions/compare/6.12.0...6.12.1
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e5dbb2ad..7c2fd0ff 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ - + From 22739486ee107562c72d02a46190c651e59a753c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:01:46 +0000 Subject: [PATCH 205/316] chore(deps): update dependency xunit to 2.9.2 (#303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [xunit](https://redirect.github.com/xunit/xunit) | `2.9.0` -> `2.9.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit/2.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit/2.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit/2.9.0/2.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit/2.9.0/2.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c2fd0ff..b41975c2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - +
From 3955b1604d5dad9b67e01974d96d53d5cacb9aad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:08:05 +0000 Subject: [PATCH 206/316] chore(deps): update dotnet monorepo (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | Type | Update | |---|---|---|---|---|---|---|---| | [Microsoft.Extensions.Logging.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `8.0.1` -> `8.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.1/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.1/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [dotnet-sdk](https://redirect.github.com/dotnet/sdk) | `8.0.401` -> `8.0.404` | [![age](https://developer.mend.io/api/mc/badges/age/dotnet-version/dotnet-sdk/8.0.404?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/dotnet-version/dotnet-sdk/8.0.404?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/dotnet-version/dotnet-sdk/8.0.401/8.0.404?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/dotnet-version/dotnet-sdk/8.0.401/8.0.404?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dotnet-sdk | patch | --- ### Release Notes
dotnet/runtime (Microsoft.Extensions.Logging.Abstractions) ### [`v8.0.2`](https://redirect.github.com/dotnet/runtime/releases/tag/v8.0.2): .NET 8.0.2 [Release](https://redirect.github.com/dotnet/core/releases/tag/v8.0.2)
dotnet/sdk (dotnet-sdk) ### [`v8.0.404`](https://redirect.github.com/dotnet/sdk/releases/tag/v8.0.404): .NET 8.0.11 [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v8.0.403...v8.0.404) [Release](https://redirect.github.com/dotnet/core/releases/tag/v8.0.11) #### What's Changed - \[release/6.0.4xx] Update dependencies from dotnet/format by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43252](https://redirect.github.com/dotnet/sdk/pull/43252) - \[release/6.0.4xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43395](https://redirect.github.com/dotnet/sdk/pull/43395) - \[release/6.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43394](https://redirect.github.com/dotnet/sdk/pull/43394) - \[automated] Merge branch 'release/6.0.1xx' => 'release/6.0.4xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/43401](https://redirect.github.com/dotnet/sdk/pull/43401) - \[automated] Merge branch 'release/6.0.4xx' => 'release/8.0.1xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/43400](https://redirect.github.com/dotnet/sdk/pull/43400) - \[release/8.0.3xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43418](https://redirect.github.com/dotnet/sdk/pull/43418) - \[release/8.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43443](https://redirect.github.com/dotnet/sdk/pull/43443) - Update branding to 8.0.111 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43822](https://redirect.github.com/dotnet/sdk/pull/43822) - Update branding to 8.0.307 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43823](https://redirect.github.com/dotnet/sdk/pull/43823) - Update branding to 8.0.404 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43824](https://redirect.github.com/dotnet/sdk/pull/43824) - \[release/8.0.4xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43422](https://redirect.github.com/dotnet/sdk/pull/43422) - \[release/8.0.1xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43825](https://redirect.github.com/dotnet/sdk/pull/43825) - \[release/8.0.4xx] Update dependencies from dotnet/fsharp by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43771](https://redirect.github.com/dotnet/sdk/pull/43771) - \[release/8.0.3xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43417](https://redirect.github.com/dotnet/sdk/pull/43417) - \[release/8.0.3xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43863](https://redirect.github.com/dotnet/sdk/pull/43863) - \[release/8.0.4xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43865](https://redirect.github.com/dotnet/sdk/pull/43865) - \[release/8.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43850](https://redirect.github.com/dotnet/sdk/pull/43850) - \[release/8.0.1xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43851](https://redirect.github.com/dotnet/sdk/pull/43851) - \[release/8.0.4xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43852](https://redirect.github.com/dotnet/sdk/pull/43852) - \[release/8.0.4xx] Update dependencies from dotnet/razor by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43849](https://redirect.github.com/dotnet/sdk/pull/43849) - Update branding to 6.0.428 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43821](https://redirect.github.com/dotnet/sdk/pull/43821) - \[release/8.0.1xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43888](https://redirect.github.com/dotnet/sdk/pull/43888) - \[release/8.0.3xx] Update dependencies from dotnet/razor by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43894](https://redirect.github.com/dotnet/sdk/pull/43894) - \[release/8.0.4xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43897](https://redirect.github.com/dotnet/sdk/pull/43897) - \[release/8.0.3xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43895](https://redirect.github.com/dotnet/sdk/pull/43895) - \[release/6.0.4xx] Update dependencies from dotnet/linker by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43641](https://redirect.github.com/dotnet/sdk/pull/43641) - \[DO NOT MERGE] Restore ability for users to consume the containers package easily by [@​surayya-MS](https://redirect.github.com/surayya-MS) in [https://github.com/dotnet/sdk/pull/43794](https://redirect.github.com/dotnet/sdk/pull/43794) - \[automated] Merge branch 'release/8.0.1xx' => 'release/8.0.3xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/43843](https://redirect.github.com/dotnet/sdk/pull/43843) - \[release/8.0.4xx] \[Containers] Fix parsing and error reporting of ports that lack port type metadata by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/43934](https://redirect.github.com/dotnet/sdk/pull/43934) - \[release/8.0.3xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43919](https://redirect.github.com/dotnet/sdk/pull/43919) - \[release/8.0.4xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43924](https://redirect.github.com/dotnet/sdk/pull/43924) - \[release/8.0.3xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43922](https://redirect.github.com/dotnet/sdk/pull/43922) - \[release/8.0.4xx] Allow reading of OCI Image Indexes to determine RID-specific base image for multi-architecture images by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/43914](https://redirect.github.com/dotnet/sdk/pull/43914) - \[release/8.0.4xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43968](https://redirect.github.com/dotnet/sdk/pull/43968) - \[release/8.0.3xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43966](https://redirect.github.com/dotnet/sdk/pull/43966) - \[release/8.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/43965](https://redirect.github.com/dotnet/sdk/pull/43965) - Expand EOL list to include net7 6 months after it goes OOS per design by [@​marcpopMSFT](https://redirect.github.com/marcpopMSFT) in [https://github.com/dotnet/sdk/pull/43440](https://redirect.github.com/dotnet/sdk/pull/43440) - Merging internal commits for release/6.0.4xx by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43981](https://redirect.github.com/dotnet/sdk/pull/43981) - Merging internal commits for release/8.0.4xx by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43985](https://redirect.github.com/dotnet/sdk/pull/43985) - Merging internal commits for release/8.0.3xx by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43983](https://redirect.github.com/dotnet/sdk/pull/43983) - \[automated] Merge branch 'release/6.0.4xx' => 'release/8.0.1xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/44031](https://redirect.github.com/dotnet/sdk/pull/44031) - \[automated] Merge branch 'release/8.0.1xx' => 'release/8.0.3xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/44030](https://redirect.github.com/dotnet/sdk/pull/44030) - \[release/8.0.3xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44013](https://redirect.github.com/dotnet/sdk/pull/44013) - \[release/8.0.4xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44015](https://redirect.github.com/dotnet/sdk/pull/44015) - \[release/8.0.4xx] Update dependencies from dotnet/fsharp by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44016](https://redirect.github.com/dotnet/sdk/pull/44016) - \[automated] Merge branch 'release/8.0.3xx' => 'release/8.0.4xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/44037](https://redirect.github.com/dotnet/sdk/pull/44037) - Merging internal commits for release/8.0.1xx by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/43982](https://redirect.github.com/dotnet/sdk/pull/43982) - \[automated] Merge branch 'release/8.0.1xx' => 'release/8.0.3xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/44045](https://redirect.github.com/dotnet/sdk/pull/44045) - \[automated] Merge branch 'release/8.0.3xx' => 'release/8.0.4xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/44057](https://redirect.github.com/dotnet/sdk/pull/44057) - \[release/8.0.4xx] Update dependencies from dotnet/fsharp by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44064](https://redirect.github.com/dotnet/sdk/pull/44064) - \[release/8.0.4xx] Add Dev Device ID by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/43362](https://redirect.github.com/dotnet/sdk/pull/43362) - \[release/8.0.3xx] Add Dev Device ID by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/43360](https://redirect.github.com/dotnet/sdk/pull/43360) - \[release/8.0.3xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44062](https://redirect.github.com/dotnet/sdk/pull/44062) - \[release/8.0.4xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44065](https://redirect.github.com/dotnet/sdk/pull/44065) - \[release/8.0.4xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44100](https://redirect.github.com/dotnet/sdk/pull/44100) - \[release/8.0.4xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44101](https://redirect.github.com/dotnet/sdk/pull/44101) - \[automated] Merge branch 'release/8.0.3xx' => 'release/8.0.4xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/44085](https://redirect.github.com/dotnet/sdk/pull/44085) - \[release/8.0.4xx] Update dependencies from dotnet/source-build-externals by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44102](https://redirect.github.com/dotnet/sdk/pull/44102) - \[release/8.0.4xx] Update dependencies from dotnet/razor by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44133](https://redirect.github.com/dotnet/sdk/pull/44133) - Switch back to 17.8.3 for the minimum msbuild version by [@​marcpopMSFT](https://redirect.github.com/marcpopMSFT) in [https://github.com/dotnet/sdk/pull/43995](https://redirect.github.com/dotnet/sdk/pull/43995) - \[release/8.0.4xx] Update dependencies from dotnet/razor by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44189](https://redirect.github.com/dotnet/sdk/pull/44189) - \[release/8.0.4xx] Update dependencies from dotnet/razor by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44227](https://redirect.github.com/dotnet/sdk/pull/44227) - \[release/8.0.4xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/44245](https://redirect.github.com/dotnet/sdk/pull/44245) - \[release/8.0.4xx] \[Containers] Fix insecure registry handling to use the correct port for the HTTP protocol by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/44234](https://redirect.github.com/dotnet/sdk/pull/44234) **Full Changelog**: https://github.com/dotnet/sdk/compare/v8.0.403...v8.0.404 ### [`v8.0.403`](https://redirect.github.com/dotnet/sdk/compare/v8.0.402...v8.0.403) [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v8.0.402...v8.0.403) ### [`v8.0.402`](https://redirect.github.com/dotnet/sdk/compare/v8.0.401...v8.0.402) [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v8.0.401...v8.0.402)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] 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> --- Directory.Packages.props | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b41975c2..e9358823 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/global.json b/global.json index caa8a0e4..0ee5ec22 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.401" + "version": "8.0.404" } } From 4b92528bd56541ca3701bd4cf80467cdda80f046 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:13:28 +0000 Subject: [PATCH 207/316] chore(deps): update codecov/codecov-action action to v4.6.0 (#306) 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://redirect.github.com/codecov/codecov-action) | action | minor | `v4.5.0` -> `v4.6.0` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v4.6.0`](https://redirect.github.com/codecov/codecov-action/releases/tag/v4.6.0) [Compare Source](https://redirect.github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0) #### What's Changed - build(deps): bump github/codeql-action from 3.25.8 to 3.25.10 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1481](https://redirect.github.com/codecov/codecov-action/pull/1481) - build(deps): bump actions/checkout from 4.1.6 to 4.1.7 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1480](https://redirect.github.com/codecov/codecov-action/pull/1480) - build(deps-dev): bump ts-jest from 29.1.4 to 29.1.5 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1479](https://redirect.github.com/codecov/codecov-action/pull/1479) - build(deps-dev): bump [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 7.13.0 to 7.13.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1485](https://redirect.github.com/codecov/codecov-action/pull/1485) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://redirect.github.com/typescript-eslint/eslint-plugin) from 7.13.0 to 7.13.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1484](https://redirect.github.com/codecov/codecov-action/pull/1484) - build(deps-dev): bump typescript from 5.4.5 to 5.5.2 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1490](https://redirect.github.com/codecov/codecov-action/pull/1490) - build(deps-dev): bump [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 7.13.1 to 7.14.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1493](https://redirect.github.com/codecov/codecov-action/pull/1493) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://redirect.github.com/typescript-eslint/eslint-plugin) from 7.13.1 to 7.14.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1492](https://redirect.github.com/codecov/codecov-action/pull/1492) - build(deps): bump github/codeql-action from 3.25.10 to 3.25.11 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1496](https://redirect.github.com/codecov/codecov-action/pull/1496) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://redirect.github.com/typescript-eslint/eslint-plugin) from 7.14.1 to 7.15.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1501](https://redirect.github.com/codecov/codecov-action/pull/1501) - build(deps-dev): bump typescript from 5.5.2 to 5.5.3 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1500](https://redirect.github.com/codecov/codecov-action/pull/1500) - build(deps-dev): bump [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 7.14.1 to 7.15.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1499](https://redirect.github.com/codecov/codecov-action/pull/1499) - build(deps): bump actions/upload-artifact from 4.3.3 to 4.3.4 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1502](https://redirect.github.com/codecov/codecov-action/pull/1502) - build(deps-dev): bump ts-jest from 29.1.5 to 29.2.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1504](https://redirect.github.com/codecov/codecov-action/pull/1504) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://redirect.github.com/typescript-eslint/eslint-plugin) from 7.15.0 to 7.16.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1503](https://redirect.github.com/codecov/codecov-action/pull/1503) - build(deps-dev): bump ts-jest from 29.2.0 to 29.2.2 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1507](https://redirect.github.com/codecov/codecov-action/pull/1507) - build(deps-dev): bump [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 7.15.0 to 7.16.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1505](https://redirect.github.com/codecov/codecov-action/pull/1505) - build(deps): bump github/codeql-action from 3.25.11 to 3.25.12 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1509](https://redirect.github.com/codecov/codecov-action/pull/1509) - chore(ci): restrict scorecards to codecov/codecov-action by [@​thomasrockhu-codecov](https://redirect.github.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1512](https://redirect.github.com/codecov/codecov-action/pull/1512) - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://redirect.github.com/typescript-eslint/eslint-plugin) from 7.16.0 to 7.16.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1514](https://redirect.github.com/codecov/codecov-action/pull/1514) - build(deps-dev): bump [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 7.16.0 to 7.16.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1513](https://redirect.github.com/codecov/codecov-action/pull/1513) - test: `versionInfo` by [@​marcobiedermann](https://redirect.github.com/marcobiedermann) in [https://github.com/codecov/codecov-action/pull/1407](https://redirect.github.com/codecov/codecov-action/pull/1407) - build(deps-dev): bump ts-jest from 29.2.2 to 29.2.3 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1515](https://redirect.github.com/codecov/codecov-action/pull/1515) - build(deps): bump github/codeql-action from 3.25.12 to 3.25.13 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1516](https://redirect.github.com/codecov/codecov-action/pull/1516) - build(deps-dev): bump typescript from 5.5.3 to 5.5.4 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1521](https://redirect.github.com/codecov/codecov-action/pull/1521) - build(deps-dev): bump [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 7.16.1 to 7.17.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1520](https://redirect.github.com/codecov/codecov-action/pull/1520) - build(deps-dev): bump [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 7.17.0 to 7.18.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1528](https://redirect.github.com/codecov/codecov-action/pull/1528) - build(deps): bump github/codeql-action from 3.25.13 to 3.25.15 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1526](https://redirect.github.com/codecov/codecov-action/pull/1526) - build(deps): bump ossf/scorecard-action from 2.3.3 to 2.4.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1525](https://redirect.github.com/codecov/codecov-action/pull/1525) - build(deps-dev): bump ts-jest from 29.2.3 to 29.2.4 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1532](https://redirect.github.com/codecov/codecov-action/pull/1532) - build(deps): bump actions/upload-artifact from 4.3.4 to 4.3.5 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1534](https://redirect.github.com/codecov/codecov-action/pull/1534) - build(deps): bump github/codeql-action from 3.25.15 to 3.26.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1542](https://redirect.github.com/codecov/codecov-action/pull/1542) - build(deps): bump actions/upload-artifact from 4.3.5 to 4.3.6 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1541](https://redirect.github.com/codecov/codecov-action/pull/1541) - ref: Tidy up types and remove string coercion by [@​nicholas-codecov](https://redirect.github.com/nicholas-codecov) in [https://github.com/codecov/codecov-action/pull/1536](https://redirect.github.com/codecov/codecov-action/pull/1536) - build(deps-dev): bump [@​octokit/webhooks-types](https://redirect.github.com/octokit/webhooks-types) from 3.77.1 to 7.5.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1545](https://redirect.github.com/codecov/codecov-action/pull/1545) - build(deps): bump github/codeql-action from 3.26.0 to 3.26.2 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1551](https://redirect.github.com/codecov/codecov-action/pull/1551) - feat: pass tokenless value as branch override by [@​joseph-sentry](https://redirect.github.com/joseph-sentry) in [https://github.com/codecov/codecov-action/pull/1511](https://redirect.github.com/codecov/codecov-action/pull/1511) - build(deps): bump actions/upload-artifact from 4.3.6 to 4.4.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1563](https://redirect.github.com/codecov/codecov-action/pull/1563) - Create makefile.yml by [@​Hawthorne001](https://redirect.github.com/Hawthorne001) in [https://github.com/codecov/codecov-action/pull/1555](https://redirect.github.com/codecov/codecov-action/pull/1555) - build(deps): bump github/codeql-action from 3.26.2 to 3.26.6 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1562](https://redirect.github.com/codecov/codecov-action/pull/1562) - build(deps-dev): bump ts-jest from 29.2.4 to 29.2.5 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1557](https://redirect.github.com/codecov/codecov-action/pull/1557) - Spell `evenName` in the logs correctly by [@​webknjaz](https://redirect.github.com/webknjaz) in [https://github.com/codecov/codecov-action/pull/1560](https://redirect.github.com/codecov/codecov-action/pull/1560) - build(deps-dev): bump typescript from 5.5.4 to 5.6.2 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1566](https://redirect.github.com/codecov/codecov-action/pull/1566) - build(deps-dev): bump [@​types/jest](https://redirect.github.com/types/jest) from 29.5.12 to 29.5.13 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1567](https://redirect.github.com/codecov/codecov-action/pull/1567) - build(deps): bump github/codeql-action from 3.26.6 to 3.26.7 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1569](https://redirect.github.com/codecov/codecov-action/pull/1569) - build(deps-dev): bump eslint from 8.57.0 to 8.57.1 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1571](https://redirect.github.com/codecov/codecov-action/pull/1571) - build(deps): bump github/codeql-action from 3.26.7 to 3.26.8 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1575](https://redirect.github.com/codecov/codecov-action/pull/1575) - build(deps-dev): bump [@​vercel/ncc](https://redirect.github.com/vercel/ncc) from 0.38.1 to 0.38.2 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1577](https://redirect.github.com/codecov/codecov-action/pull/1577) - chore: fix typo of OSS by [@​shoothzj](https://redirect.github.com/shoothzj) in [https://github.com/codecov/codecov-action/pull/1578](https://redirect.github.com/codecov/codecov-action/pull/1578) - build(deps): bump github/codeql-action from 3.26.8 to 3.26.9 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1584](https://redirect.github.com/codecov/codecov-action/pull/1584) - build(deps): bump actions/checkout from 4.1.7 to 4.2.0 by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1583](https://redirect.github.com/codecov/codecov-action/pull/1583) - fix: bump eslint parser deps by [@​thomasrockhu-codecov](https://redirect.github.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1586](https://redirect.github.com/codecov/codecov-action/pull/1586) - chore(release):4.6.0 by [@​thomasrockhu-codecov](https://redirect.github.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1587](https://redirect.github.com/codecov/codecov-action/pull/1587) #### New Contributors - [@​nicholas-codecov](https://redirect.github.com/nicholas-codecov) made their first contribution in [https://github.com/codecov/codecov-action/pull/1536](https://redirect.github.com/codecov/codecov-action/pull/1536) - [@​Hawthorne001](https://redirect.github.com/Hawthorne001) made their first contribution in [https://github.com/codecov/codecov-action/pull/1555](https://redirect.github.com/codecov/codecov-action/pull/1555) - [@​webknjaz](https://redirect.github.com/webknjaz) made their first contribution in [https://github.com/codecov/codecov-action/pull/1560](https://redirect.github.com/codecov/codecov-action/pull/1560) - [@​shoothzj](https://redirect.github.com/shoothzj) made their first contribution in [https://github.com/codecov/codecov-action/pull/1578](https://redirect.github.com/codecov/codecov-action/pull/1578) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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 83d837eb..8052e6a3 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -36,7 +36,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v4.5.0 + - uses: codecov/codecov-action@v4.6.0 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From 87f9cfa9b5ace84546690fea95f33bf06fd1947b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:22:43 +0000 Subject: [PATCH 208/316] chore(deps): update dependency nsubstitute to 5.3.0 (#311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [NSubstitute](https://nsubstitute.github.io/) ([source](https://redirect.github.com/nsubstitute/NSubstitute)) | `5.1.0` -> `5.3.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/NSubstitute/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/NSubstitute/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/NSubstitute/5.1.0/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/NSubstitute/5.1.0/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
nsubstitute/NSubstitute (NSubstitute) ### [`v5.3.0`](https://redirect.github.com/nsubstitute/NSubstitute/blob/HEAD/CHANGELOG.md#530-October-2024) - \[NEW] Introduced `Substitute.ForTypeForwardingTo` to create substitutes that forward interceptable calls to a concrete class. This provides an easy way of implementing a test spy over an existing type. Designed and implemented by [@​marcoregueira](https://redirect.github.com/marcoregueira) in [https://github.com/nsubstitute/NSubstitute/pull/700](https://redirect.github.com/nsubstitute/NSubstitute/pull/700) from a proposal by [@​wsaeed](https://redirect.github.com/wsaeed). Thanks to all who contributed to discussions of this feature. - \[NEW] Support Raise.EventWith default constructor ([#​788](https://redirect.github.com/nsubstitute/NSubstitute/issues/788)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/813](https://redirect.github.com/nsubstitute/NSubstitute/pull/813) - \[NEW] Implement When(...).Throws to avoid confusion with Throw method ([#​803](https://redirect.github.com/nsubstitute/NSubstitute/issues/803)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/818](https://redirect.github.com/nsubstitute/NSubstitute/pull/818) - \[FIX] Arg.Any\() does not match arguments passed by reference ([#​787](https://redirect.github.com/nsubstitute/NSubstitute/issues/787)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/811](https://redirect.github.com/nsubstitute/NSubstitute/pull/811) - \[FIX] Support matching arguments whose type is generic, when their concrete type is not known ([#​786](https://redirect.github.com/nsubstitute/NSubstitute/issues/786)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/814](https://redirect.github.com/nsubstitute/NSubstitute/pull/814) - \[FIX] Release build workflow [https://github.com/nsubstitute/NSubstitute/pull/797](https://redirect.github.com/nsubstitute/NSubstitute/pull/797)7) - \[DOC] Add Throws for exceptions to the docs by [@​304NotModified](https://redirect.github.com/304NotModified) in [https://github.com/nsubstitute/NSubstitute/pull/795](https://redirect.github.com/nsubstitute/NSubstitute/pull/795) - \[DOC] Remove Visual Studio for Mac from readme by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/807](https://redirect.github.com/nsubstitute/NSubstitute/pull/807) - \[TECH] Migrate from NUnit 3 to NUnit 4 by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/783](https://redirect.github.com/nsubstitute/NSubstitute/pull/783) - \[TECH] Update build project to .net 8 by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/776](https://redirect.github.com/nsubstitute/NSubstitute/pull/776) - \[TECH] Code style: use C# 12 collection literals by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/810](https://redirect.github.com/nsubstitute/NSubstitute/pull/810) - \[TECH] Use c# 12 primary constructors by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/812](https://redirect.github.com/nsubstitute/NSubstitute/pull/812) - \[TECH] Added csharp_style_prefer_primary_constructors into editorconfig by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/819](https://redirect.github.com/nsubstitute/NSubstitute/pull/819) Thanks to first-time contributors [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) and [@​marcoregueira](https://redirect.github.com/marcoregueira)! Thanks also [@​304NotModified](https://redirect.github.com/304NotModified) and [@​Romfos](https://redirect.github.com/Romfos) for their continued support and contributions to this release.
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e9358823..ef5cde51 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + From ccf02506ecd924738b6ae03dedf25c8e2df6d1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Nov 2024 20:55:06 +0000 Subject: [PATCH 209/316] fix: Fix action syntax in workflow configuration (#315) --- .github/workflows/code-coverage.yml | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 8052e6a3..58e74f1f 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -2,19 +2,19 @@ name: Code Coverage on: push: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" pull_request: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" jobs: build-test-report: strategy: matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} @@ -23,22 +23,22 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 8.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: | + 6.0.x + 8.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v4.6.0 - with: - name: Code Coverage for ${{ matrix.os }} - fail_ci_if_error: true - verbose: true - token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} + - uses: codecov/codecov-action@v4.6.0 + with: + name: Code Coverage for ${{ matrix.os }} + fail_ci_if_error: true + verbose: true + token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} From 1aaa0ec0e75d5048554752db30193694f0999a4a Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 18 Nov 2024 18:27:54 +0400 Subject: [PATCH 210/316] feat: Add Dependency Injection and Hosting support for OpenFeature (#310) This pull request introduces key features and improvements to the OpenFeature project, focusing on Dependency Injection and Hosting support: - **OpenFeature.DependencyInjection Project:** - Implemented `OpenFeatureBuilder`, including `OpenFeatureBuilderExtensions` for seamless integration. - Added `IFeatureLifecycleManager` interface and its implementation. - Introduced `AddProvider` extension method for easy provider configuration. - Created `OpenFeatureServiceCollectionExtensions` for service registration. - **OpenFeature.Hosting Project:** - Added `HostedFeatureLifecycleService` to manage the lifecycle of feature providers in hosted environments. - **Testing Enhancements:** - Created unit tests for critical methods, including `OpenFeatureBuilderExtensionsTests` and `OpenFeatureServiceCollectionExtensionsTests`. - Replicated and tested `NoOpFeatureProvider` implementation for better test coverage. These changes significantly improve OpenFeature's extensibility and lifecycle management for feature providers within Dependency Injection (DI) and hosted environments. Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 16 +- OpenFeature.sln | 35 ++- README.md | 77 ++++- .../Diagnostics/FeatureCodes.cs | 38 +++ src/OpenFeature.DependencyInjection/Guard.cs | 20 ++ .../IFeatureLifecycleManager.cs | 24 ++ .../IFeatureProviderFactory.cs | 23 ++ .../Internal/FeatureLifecycleManager.cs | 51 ++++ .../CallerArgumentExpressionAttribute.cs | 23 ++ .../MultiTarget/IsExternalInit.cs | 21 ++ .../OpenFeature.DependencyInjection.csproj | 28 ++ .../OpenFeatureBuilder.cs | 60 ++++ .../OpenFeatureBuilderExtensions.cs | 280 ++++++++++++++++++ .../OpenFeatureOptions.cs | 49 +++ .../OpenFeatureServiceCollectionExtensions.cs | 68 +++++ .../PolicyNameOptions.cs | 12 + .../Memory/FeatureBuilderExtensions.cs | 55 ++++ .../Memory/InMemoryProviderFactory.cs | 34 +++ .../FeatureLifecycleStateOptions.cs | 18 ++ src/OpenFeature.Hosting/FeatureStartState.cs | 22 ++ src/OpenFeature.Hosting/FeatureStopState.cs | 22 ++ .../HostedFeatureLifecycleService.cs | 100 +++++++ .../OpenFeature.Hosting.csproj | 18 ++ .../OpenFeatureBuilderExtensions.cs | 38 +++ .../FeatureLifecycleManagerTests.cs | 64 ++++ .../NoOpFeatureProvider.cs | 52 ++++ .../NoOpFeatureProviderFactory.cs | 9 + .../NoOpProvider.cs | 8 + ...enFeature.DependencyInjection.Tests.csproj | 37 +++ .../OpenFeatureBuilderExtensionsTests.cs | 98 ++++++ ...FeatureServiceCollectionExtensionsTests.cs | 39 +++ 31 files changed, 1425 insertions(+), 14 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs create mode 100644 src/OpenFeature.DependencyInjection/Guard.cs create mode 100644 src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs create mode 100644 src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs create mode 100644 src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs create mode 100644 src/OpenFeature.DependencyInjection/PolicyNameOptions.cs create mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs create mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs create mode 100644 src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs create mode 100644 src/OpenFeature.Hosting/FeatureStartState.cs create mode 100644 src/OpenFeature.Hosting/FeatureStopState.cs create mode 100644 src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs create mode 100644 src/OpenFeature.Hosting/OpenFeature.Hosting.csproj create mode 100644 src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ef5cde51..ab66dbb9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,17 +1,20 @@ - + true - + - + + + + - + @@ -26,10 +29,11 @@ + - + - + diff --git a/OpenFeature.sln b/OpenFeature.sln index 6f1cce8d..e8191acd 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -77,7 +77,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -101,21 +107,36 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} {07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/README.md b/README.md index b8f25012..4f2193bf 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,9 @@ public async Task Example() | βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | | βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| πŸ”¬ | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. | -> Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ +> Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ | Experimental: πŸ”¬ ### Providers @@ -300,6 +301,80 @@ public class MyHook : Hook 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 +> [!NOTE] +> The OpenFeature.DependencyInjection and OpenFeature.Hosting packages are currently experimental. They streamline the integration of OpenFeature within .NET applications, allowing for seamless configuration and lifecycle management of feature flag providers using dependency injection and hosting services. + +#### Installation +To set up dependency injection and hosting capabilities for OpenFeature, install the following packages: +```sh +dotnet add package OpenFeature.DependencyInjection +dotnet add package OpenFeature.Hosting +``` +#### Usage Examples +For a basic configuration, you can use the InMemoryProvider. This provider is simple and well-suited for development and testing purposes. + +**Basic Configuration:** +```csharp +builder.Services.AddOpenFeature(featureBuilder => { + featureBuilder + .AddHostedFeatureLifecycle() // From Hosting package + .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ }) + .AddInMemoryProvider(); +}); +``` +**Domain-Scoped Provider Configuration:** +
To set up multiple providers with a selection policy, define logic for choosing the default provider. This example designates `name1` as the default provider: +```csharp +builder.Services.AddOpenFeature(featureBuilder => { + featureBuilder + .AddHostedFeatureLifecycle() + .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ }) + .AddInMemoryProvider("name1") + .AddInMemoryProvider("name2") + .AddPolicyName(options => { + // Custom logic to select a default provider + options.DefaultNameSelector = serviceProvider => "name1"; + }); +}); +``` +#### Creating a New Provider +To integrate a custom provider, such as InMemoryProvider, you’ll need to create a factory that builds and configures the provider. This section demonstrates how to set up InMemoryProvider as a new provider with custom configuration options. + +**Configuring InMemoryProvider as a New Provider** +
Begin by creating a custom factory class, `InMemoryProviderFactory`, that implements `IFeatureProviderFactory`. This factory will initialize your provider with any necessary configurations. +```csharp +public class InMemoryProviderFactory : IFeatureProviderFactory +{ + internal IDictionary? Flags { get; set; } + + public FeatureProvider Create() => new InMemoryProvider(Flags); +} +``` +**Adding an Extension Method to OpenFeatureBuilder** +
To streamline the configuration process, add an extension method, `AddInMemoryProvider`, to `OpenFeatureBuilder`. This allows you to set up the provider with either a domain-scoped or a default configuration. + +```csharp +public static partial class FeatureBuilderExtensions +{ + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null) + => builder.AddProvider(factory => ConfigureFlags(factory, configure)); + + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null) + => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure)); + + private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure) + { + if (configure == null) + return; + + var flag = new Dictionary(); + configure.Invoke(flag); + factory.Flags = flag; + } +} +``` + ## ⭐️ Support the project diff --git a/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs b/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs new file mode 100644 index 00000000..582ab39c --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs @@ -0,0 +1,38 @@ +namespace OpenFeature.DependencyInjection.Diagnostics; + +/// +/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework. +/// +/// +/// Experimental - This class includes identifiers that allow developers to track and conditionally enable +/// experimental features. Each identifier follows a structured code format to indicate the feature domain, +/// maturity level, and unique identifier. Note that experimental features are subject to change or removal +/// in future releases. +/// +/// Basic Information
+/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize +/// and manage experimental features effectively. +///
+///
+/// +/// +/// Code Structure: +/// - "OF" - Represents the OpenFeature library. +/// - "DI" - Indicates the Dependency Injection domain. +/// - "001" - Unique identifier for a specific feature. +/// +/// +internal static class FeatureCodes +{ + /// + /// Identifier for the experimental Dependency Injection features within the OpenFeature framework. + /// + /// + /// OFDI001 identifier marks experimental features in the Dependency Injection (DI) domain. + /// + /// Usage: + /// Developers can use this identifier to conditionally enable or test experimental DI features. + /// It is part of the OpenFeature diagnostics system to help track experimental functionality. + /// + public const string NewDi = "OFDI001"; +} diff --git a/src/OpenFeature.DependencyInjection/Guard.cs b/src/OpenFeature.DependencyInjection/Guard.cs new file mode 100644 index 00000000..337a8290 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Guard.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace OpenFeature.DependencyInjection; + +[DebuggerStepThrough] +internal static class Guard +{ + public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } + + public static void ThrowIfNullOrWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (string.IsNullOrWhiteSpace(argument)) + throw new ArgumentNullException(paramName); + } +} diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs new file mode 100644 index 00000000..4891f2e8 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs @@ -0,0 +1,24 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Defines the contract for managing the lifecycle of a feature api. +/// +public interface IFeatureLifecycleManager +{ + /// + /// Ensures that the feature provider is properly initialized and ready to be used. + /// This method should handle all necessary checks, configuration, and setup required to prepare the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of initializing the feature provider. + /// Thrown when the feature provider is not registered or is in an invalid state. + ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default); + + /// + /// Gracefully shuts down the feature api, ensuring all resources are properly disposed of and any persistent state is saved. + /// This method should handle all necessary cleanup and shutdown operations for the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of shutting down the feature provider. + ValueTask ShutdownAsync(CancellationToken cancellationToken = default); +} diff --git a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs new file mode 100644 index 00000000..8c40cee3 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs @@ -0,0 +1,23 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Provides a contract for creating instances of . +/// This factory interface enables custom configuration and initialization of feature providers +/// to support domain-specific or application-specific feature flag management. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public interface IFeatureProviderFactory +{ + /// + /// Creates an instance of a configured according to + /// the specific settings implemented by the concrete factory. + /// + /// + /// A new instance of . + /// The configuration and behavior of this provider instance are determined by + /// the implementation of this method. + /// + FeatureProvider Create(); +} diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs new file mode 100644 index 00000000..d14d421b --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenFeature.DependencyInjection.Internal; + +internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager +{ + private readonly Api _featureApi; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger logger) + { + _featureApi = featureApi; + _serviceProvider = serviceProvider; + _logger = logger; + } + + /// + public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default) + { + this.LogStartingInitializationOfFeatureProvider(); + + var options = _serviceProvider.GetRequiredService>().Value; + if (options.HasDefaultProvider) + { + var featureProvider = _serviceProvider.GetRequiredService(); + await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); + } + + foreach (var name in options.ProviderNames) + { + var featureProvider = _serviceProvider.GetRequiredKeyedService(name); + await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false); + } + } + + /// + public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) + { + this.LogShuttingDownFeatureProvider(); + await _featureApi.ShutdownAsync().ConfigureAwait(false); + } + + [LoggerMessage(200, LogLevel.Information, "Starting initialization of the feature provider")] + partial void LogStartingInitializationOfFeatureProvider(); + + [LoggerMessage(200, LogLevel.Information, "Shutting down the feature provider")] + partial void LogShuttingDownFeatureProvider(); +} diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..afbec6b0 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,23 @@ +// @formatter:off +// ReSharper disable All +#if NETCOREAPP3_0_OR_GREATER +// https://github.com/dotnet/runtime/issues/96197 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))] +#else +#pragma warning disable +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } +} +#endif diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs new file mode 100644 index 00000000..87714111 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs @@ -0,0 +1,21 @@ +// @formatter:off +// ReSharper disable All +#if NET5_0_OR_GREATER +// https://github.com/dotnet/runtime/issues/96197 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] +#else +#pragma warning disable +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +static class IsExternalInit { } +#endif diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj new file mode 100644 index 00000000..895c45f3 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -0,0 +1,28 @@ +ο»Ώ + + + netstandard2.0;net6.0;net8.0;net462 + enable + enable + OpenFeature.DependencyInjection + + + + + + + + + + + + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs new file mode 100644 index 00000000..ae1e8c8f --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature.DependencyInjection; + +/// +/// Describes a backed by an . +/// +/// The services being configured. +public class OpenFeatureBuilder(IServiceCollection services) +{ + /// The services being configured. + public IServiceCollection Services { get; } = services; + + /// + /// Indicates whether the evaluation context has been configured. + /// This property is used to determine if specific configurations or services + /// should be initialized based on the presence of an evaluation context. + /// + public bool IsContextConfigured { get; internal set; } + + /// + /// Indicates whether the policy has been configured. + /// + public bool IsPolicyConfigured { get; internal set; } + + /// + /// Gets a value indicating whether a default provider has been registered. + /// + public bool HasDefaultProvider { get; internal set; } + + /// + /// Gets the count of domain-bound providers that have been registered. + /// This count does not include the default provider. + /// + public int DomainBoundProviderRegistrationCount { get; internal set; } + + /// + /// Validates the current configuration, ensuring that a policy is set when multiple providers are registered + /// or when a default provider is registered alongside another provider. + /// + /// + /// Thrown if multiple providers are registered without a policy, or if both a default provider + /// and an additional provider are registered without a policy configuration. + /// + public void Validate() + { + if (!IsPolicyConfigured) + { + if (DomainBoundProviderRegistrationCount > 1) + { + throw new InvalidOperationException("Multiple providers have been registered, but no policy has been configured."); + } + + if (HasDefaultProvider && DomainBoundProviderRegistrationCount == 1) + { + throw new InvalidOperationException("A default provider and an additional provider have been registered without a policy configuration."); + } + } + } +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..a494b045 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,280 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; +using OpenFeature.Model; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(DependencyInjection.Diagnostics.FeatureCodes.NewDi)] +#endif +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// This method is used to add a new context to the service collection. + /// + /// The instance. + /// the desired configuration + /// The instance. + /// Thrown when the or action is null. + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); + + return builder.AddContext((b, _) => configure(b)); + } + + /// + /// This method is used to add a new context to the service collection. + /// + /// The instance. + /// the desired configuration + /// The instance. + /// Thrown when the or action is null. + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); + + builder.IsContextConfigured = true; + builder.Services.TryAddTransient(provider => + { + var contextBuilder = EvaluationContext.Builder(); + configure(contextBuilder, provider); + return contextBuilder.Build(); + }); + + return builder; + } + + /// + /// Adds a new feature provider with specified options and configuration builder. + /// + /// The type for configuring the feature provider. + /// The type of the provider factory implementing . + /// The instance. + /// An optional action to configure the provider factory of type . + /// The instance. + /// Thrown when the is null. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) + where TOptions : OpenFeatureOptions + where TProviderFactory : class, IFeatureProviderFactory + { + Guard.ThrowIfNull(builder); + + builder.HasDefaultProvider = true; + + builder.Services.Configure(options => + { + options.AddDefaultProviderName(); + }); + + if (configureFactory != null) + { + builder.Services.AddOptions() + .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") + .Configure(configureFactory); + } + else + { + builder.Services.AddOptions() + .Configure(options => { }); + } + + builder.Services.TryAddSingleton(static provider => + { + var providerFactory = provider.GetRequiredService>().Value; + return providerFactory.Create(); + }); + + builder.AddClient(); + + return builder; + } + + /// + /// Adds a new feature provider with the default type and a specified configuration builder. + /// + /// The type of the provider factory implementing . + /// The instance. + /// An optional action to configure the provider factory of type . + /// The configured instance. + /// Thrown when the is null. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) + where TProviderFactory : class, IFeatureProviderFactory + => AddProvider(builder, configureFactory); + + /// + /// Adds a feature provider with specified options and configuration builder for the specified domain. + /// + /// The type for configuring the feature provider. + /// The type of the provider factory implementing . + /// The instance. + /// The unique name of the provider. + /// An optional action to configure the provider factory of type . + /// The instance. + /// Thrown when the or is null or empty. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) + where TOptions : OpenFeatureOptions + where TProviderFactory : class, IFeatureProviderFactory + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNullOrWhiteSpace(domain, nameof(domain)); + + builder.DomainBoundProviderRegistrationCount++; + + builder.Services.Configure(options => + { + options.AddProviderName(domain); + }); + + if (configureFactory != null) + { + builder.Services.AddOptions(domain) + .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") + .Configure(configureFactory); + } + else + { + builder.Services.AddOptions(domain) + .Configure(options => { }); + } + + builder.Services.TryAddKeyedSingleton(domain, static (provider, key) => + { + var options = provider.GetRequiredService>(); + var providerFactory = options.Get(key!.ToString()); + return providerFactory.Create(); + }); + + builder.AddClient(domain); + + return builder; + } + + /// + /// Adds a feature provider with a specified configuration builder for the specified domain, using default . + /// + /// The type of the provider factory implementing . + /// The instance. + /// The unique domain of the provider. + /// An optional action to configure the provider factory of type . + /// The configured instance. + /// Thrown when the or is null or empty. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) + where TProviderFactory : class, IFeatureProviderFactory + => AddProvider(builder, domain, configureFactory); + + /// + /// Adds a feature client to the service collection, configuring it to work with a specific context if provided. + /// + /// The instance. + /// Optional: The name for the feature client instance. + /// The instance. + internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, string? name = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + if (builder.IsContextConfigured) + { + builder.Services.TryAddScoped(static provider => + { + var api = provider.GetRequiredService(); + var client = api.GetClient(); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + builder.Services.TryAddScoped(static provider => + { + var api = provider.GetRequiredService(); + return api.GetClient(); + }); + } + } + else + { + if (builder.IsContextConfigured) + { + builder.Services.TryAddKeyedScoped(name, static (provider, key) => + { + var api = provider.GetRequiredService(); + var client = api.GetClient(key!.ToString()); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + builder.Services.TryAddKeyedScoped(name, static (provider, key) => + { + var api = provider.GetRequiredService(); + return api.GetClient(key!.ToString()); + }); + } + } + + return builder; + } + + /// + /// Configures a default client for OpenFeature using the provided factory function. + /// + /// The instance. + /// + /// A factory function that creates an based on the service provider and . + /// + /// The configured instance. + internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func clientFactory) + { + builder.Services.AddScoped(provider => + { + var policy = provider.GetRequiredService>().Value; + return clientFactory(provider, policy); + }); + + return builder; + } + + /// + /// Configures policy name options for OpenFeature using the specified options type. + /// + /// The type of options used to configure . + /// The instance. + /// A delegate to configure . + /// The configured instance. + /// Thrown when the or is null. + public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + where TOptions : PolicyNameOptions + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configureOptions); + + builder.IsPolicyConfigured = true; + + builder.Services.Configure(configureOptions); + return builder; + } + + /// + /// Configures the default policy name options for OpenFeature. + /// + /// The instance. + /// A delegate to configure . + /// The configured instance. + public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + => AddPolicyName(builder, configureOptions); +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs new file mode 100644 index 00000000..1be312ed --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -0,0 +1,49 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Options to configure OpenFeature +/// +public class OpenFeatureOptions +{ + private readonly HashSet _providerNames = []; + + /// + /// Determines if a default provider has been registered. + /// + public bool HasDefaultProvider { get; private set; } + + /// + /// The type of the configured feature provider. + /// + public Type FeatureProviderType { get; protected internal set; } = null!; + + /// + /// Gets a read-only list of registered provider names. + /// + public IReadOnlyCollection ProviderNames => _providerNames; + + /// + /// Registers the default provider name if no specific name is provided. + /// Sets to true. + /// + public void AddDefaultProviderName() => AddProviderName(null); + + /// + /// Registers a new feature provider name. This operation is thread-safe. + /// + /// The name of the feature provider to register. Registers as default if null. + public void AddProviderName(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + HasDefaultProvider = true; + } + else + { + lock (_providerNames) + { + _providerNames.Add(name!); + } + } + } +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs new file mode 100644 index 00000000..e7a503bb --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; +using OpenFeature.DependencyInjection.Internal; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +public static partial class OpenFeatureServiceCollectionExtensions +{ + /// + /// Adds and configures OpenFeature services to the provided . + /// + /// The instance. + /// A configuration action for customizing OpenFeature setup via + /// The modified instance + /// Thrown if or is null. + public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) + { + Guard.ThrowIfNull(services); + Guard.ThrowIfNull(configure); + + // Register core OpenFeature services as singletons. + services.TryAddSingleton(Api.Instance); + services.TryAddSingleton(); + + var builder = new OpenFeatureBuilder(services); + configure(builder); + + // If a default provider is specified without additional providers, + // return early as no extra configuration is needed. + if (builder.HasDefaultProvider && builder.DomainBoundProviderRegistrationCount == 0) + { + return services; + } + + // Validate builder configuration to ensure consistency and required setup. + builder.Validate(); + + if (!builder.IsPolicyConfigured) + { + // Add a default name selector policy to use the first registered provider name as the default. + builder.AddPolicyName(options => + { + options.DefaultNameSelector = provider => + { + var options = provider.GetRequiredService>().Value; + return options.ProviderNames.First(); + }; + }); + } + + builder.AddDefaultClient((provider, policy) => + { + var name = policy.DefaultNameSelector.Invoke(provider); + if (name == null) + { + return provider.GetRequiredService(); + } + return provider.GetRequiredKeyedService(name); + }); + + return services; + } +} diff --git a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs new file mode 100644 index 00000000..f77b019b --- /dev/null +++ b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs @@ -0,0 +1,12 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Options to configure the default feature client name. +/// +public class PolicyNameOptions +{ + /// + /// A delegate to select the default feature client name. + /// + public Func DefaultNameSelector { get; set; } = null!; +} diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs new file mode 100644 index 00000000..199e01b0 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs @@ -0,0 +1,55 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.DependencyInjection.Providers.Memory; + +/// +/// Extension methods for configuring feature providers with . +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public static partial class FeatureBuilderExtensions +{ + /// + /// Adds an in-memory feature provider to the with optional flag configuration. + /// + /// The instance to configure. + /// + /// An optional delegate to configure feature flags in the in-memory provider. + /// If provided, it allows setting up the initial flags. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null) + => builder.AddProvider(factory => ConfigureFlags(factory, configure)); + + /// + /// Adds an in-memory feature provider with a specific domain to the + /// with optional flag configuration. + /// + /// The instance to configure. + /// The unique domain of the provider + /// + /// An optional delegate to configure feature flags in the in-memory provider. + /// If provided, it allows setting up the initial flags. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null) + => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure)); + + /// + /// Configures the feature flags for an instance. + /// + /// The to configure. + /// + /// An optional delegate that sets up the initial flags in the provider's flag dictionary. + /// + private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure) + { + if (configure == null) + return; + + var flag = new Dictionary(); + configure.Invoke(flag); + factory.Flags = flag; + } +} diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs new file mode 100644 index 00000000..2d155dd9 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs @@ -0,0 +1,34 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.DependencyInjection.Providers.Memory; + +/// +/// A factory for creating instances of , +/// an in-memory implementation of . +/// This factory allows for the customization of feature flags to facilitate +/// testing and lightweight feature flag management without external dependencies. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public class InMemoryProviderFactory : IFeatureProviderFactory +{ + /// + /// Gets or sets the collection of feature flags used to configure the + /// instances. This dictionary maps + /// flag names to instances, enabling pre-configuration + /// of features for testing or in-memory evaluation. + /// + internal IDictionary? Flags { get; set; } + + /// + /// Creates a new instance of with the specified + /// flags set in . This instance is configured for in-memory + /// feature flag management, suitable for testing or lightweight feature toggling scenarios. + /// + /// + /// A configured that can be used to manage + /// feature flags in an in-memory context. + /// + public FeatureProvider Create() => new InMemoryProvider(Flags); +} diff --git a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs new file mode 100644 index 00000000..91e3047d --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs @@ -0,0 +1,18 @@ +namespace OpenFeature; + +/// +/// Represents the lifecycle state options for a feature, +/// defining the states during the start and stop lifecycle. +/// +public class FeatureLifecycleStateOptions +{ + /// + /// Gets or sets the state during the feature startup lifecycle. + /// + public FeatureStartState StartState { get; set; } = FeatureStartState.Starting; + + /// + /// Gets or sets the state during the feature shutdown lifecycle. + /// + public FeatureStopState StopState { get; set; } = FeatureStopState.Stopping; +} diff --git a/src/OpenFeature.Hosting/FeatureStartState.cs b/src/OpenFeature.Hosting/FeatureStartState.cs new file mode 100644 index 00000000..8001b9c2 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStartState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for starting a feature. +/// +public enum FeatureStartState +{ + /// + /// The feature is in the process of starting. + /// + Starting, + + /// + /// The feature is at the start state. + /// + Start, + + /// + /// The feature has fully started. + /// + Started +} diff --git a/src/OpenFeature.Hosting/FeatureStopState.cs b/src/OpenFeature.Hosting/FeatureStopState.cs new file mode 100644 index 00000000..d8d6a28c --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStopState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for stopping a feature. +/// +public enum FeatureStopState +{ + /// + /// The feature is in the process of stopping. + /// + Stopping, + + /// + /// The feature is at the stop state. + /// + Stop, + + /// + /// The feature has fully stopped. + /// + Stopped +} diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs new file mode 100644 index 00000000..5209a525 --- /dev/null +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; + +namespace OpenFeature.Hosting; + +/// +/// A hosted service that manages the lifecycle of features within the application. +/// It ensures that features are properly initialized when the service starts +/// and gracefully shuts down when the service stops. +/// +public sealed partial class HostedFeatureLifecycleService : IHostedLifecycleService +{ + private readonly ILogger _logger; + private readonly IFeatureLifecycleManager _featureLifecycleManager; + private readonly IOptions _featureLifecycleStateOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The logger used to log lifecycle events. + /// The feature lifecycle manager responsible for initialization and shutdown. + /// Options that define the start and stop states of the feature lifecycle. + public HostedFeatureLifecycleService( + ILogger logger, + IFeatureLifecycleManager featureLifecycleManager, + IOptions featureLifecycleStateOptions) + { + _logger = logger; + _featureLifecycleManager = featureLifecycleManager; + _featureLifecycleStateOptions = featureLifecycleStateOptions; + } + + /// + /// Ensures that the feature is properly initialized when the service starts. + /// + public async Task StartingAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Starting, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Start" state. + /// + public async Task StartAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Start, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully started and operational. + /// + public async Task StartedAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Started, cancellationToken).ConfigureAwait(false); + + /// + /// Gracefully shuts down the feature when the service is stopping. + /// + public async Task StoppingAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopping, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Stop" state. + /// + public async Task StopAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stop, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully stopped and no longer operational. + /// + public async Task StoppedAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopped, cancellationToken).ConfigureAwait(false); + + /// + /// Initializes the feature lifecycle if the current state matches the expected start state. + /// + private async Task InitializeIfStateMatchesAsync(FeatureStartState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StartState == expectedState) + { + this.LogInitializingFeatureLifecycleManager(expectedState); + await _featureLifecycleManager.EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Shuts down the feature lifecycle if the current state matches the expected stop state. + /// + private async Task ShutdownIfStateMatchesAsync(FeatureStopState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StopState == expectedState) + { + this.LogShuttingDownFeatureLifecycleManager(expectedState); + await _featureLifecycleManager.ShutdownAsync(cancellationToken).ConfigureAwait(false); + } + } + + [LoggerMessage(200, LogLevel.Information, "Initializing the Feature Lifecycle Manager for state {State}.")] + partial void LogInitializingFeatureLifecycleManager(FeatureStartState state); + + [LoggerMessage(200, LogLevel.Information, "Shutting down the Feature Lifecycle Manager for state {State}")] + partial void LogShuttingDownFeatureLifecycleManager(FeatureStopState state); +} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj new file mode 100644 index 00000000..48730084 --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -0,0 +1,18 @@ + + + + net6.0;net8.0 + enable + enable + OpenFeature + + + + + + + + + + + diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..16f437b3 --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.DependencyInjection; +using OpenFeature.Hosting; + +namespace OpenFeature; + +/// +/// Extension methods for configuring the hosted feature lifecycle in the . +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// Adds the to the OpenFeatureBuilder, + /// which manages the lifecycle of features within the application. It also allows + /// configuration of the . + /// + /// The instance. + /// An optional action to configure . + /// The instance. + public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null) + { + if (configureOptions == null) + { + builder.Services.Configure(cfg => + { + cfg.StartState = FeatureStartState.Starting; + cfg.StopState = FeatureStopState.Stopping; + }); + } + else + { + builder.Services.Configure(configureOptions); + } + + builder.Services.AddHostedService(); + return builder; + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs new file mode 100644 index 00000000..b0176bc4 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -0,0 +1,64 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using OpenFeature.DependencyInjection.Internal; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class FeatureLifecycleManagerTests +{ + private readonly FeatureLifecycleManager _systemUnderTest; + private readonly IServiceProvider _mockServiceProvider; + + public FeatureLifecycleManagerTests() + { + Api.Instance.SetContext(null); + Api.Instance.ClearHooks(); + + _mockServiceProvider = Substitute.For(); + + var options = new OpenFeatureOptions(); + options.AddDefaultProviderName(); + var optionsMock = Substitute.For>(); + optionsMock.Value.Returns(options); + + _mockServiceProvider.GetService>().Returns(optionsMock); + + _systemUnderTest = new FeatureLifecycleManager( + Api.Instance, + _mockServiceProvider, + Substitute.For>()); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExists() + { + // Arrange + var featureProvider = new NoOpFeatureProvider(); + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(featureProvider); + + // Act + await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(true); + + // Assert + Api.Instance.GetProvider().Should().BeSameAs(featureProvider); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist() + { + // Arrange + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); + + // Act + var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask(); + + // Assert + var exception = await Assert.ThrowsAsync(act).ConfigureAwait(true); + exception.Should().NotBeNull(); + exception.Message.Should().NotBeNullOrWhiteSpace(); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs new file mode 100644 index 00000000..ac3e5209 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs @@ -0,0 +1,52 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +// This class replicates the NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider.cs. +// It is used here to facilitate unit testing without relying on the internal NoOpFeatureProvider class. +// If the InternalsVisibleTo attribute is added to the OpenFeature project, +// this class can be removed and the original NoOpFeatureProvider can be directly accessed for testing. +internal sealed class NoOpFeatureProvider : FeatureProvider +{ + private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); + + public override Metadata GetMetadata() + { + return this._metadata; + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue) + { + return new ResolutionDetails( + flagKey, + defaultValue, + reason: NoOpProvider.ReasonNoOp, + variant: NoOpProvider.Variant + ); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs new file mode 100644 index 00000000..1ee14bf0 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs @@ -0,0 +1,9 @@ +namespace OpenFeature.DependencyInjection.Tests; + +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public class NoOpFeatureProviderFactory : IFeatureProviderFactory +{ + public FeatureProvider Create() => new NoOpFeatureProvider(); +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs new file mode 100644 index 00000000..7bf20bca --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs @@ -0,0 +1,8 @@ +namespace OpenFeature.DependencyInjection.Tests; + +internal static class NoOpProvider +{ + public const string NoOpProviderName = "No-op Provider"; + public const string ReasonNoOp = "No-op"; + public const string Variant = "No-op"; +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj new file mode 100644 index 00000000..9937e1bc --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -0,0 +1,37 @@ + + + + net6.0;net8.0 + enable + enable + + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs new file mode 100644 index 00000000..3f6ef227 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public partial class OpenFeatureBuilderExtensionsTests +{ + private readonly IServiceCollection _services; + private readonly OpenFeatureBuilder _systemUnderTest; + + public OpenFeatureBuilderExtensionsTests() + { + _services = new ServiceCollection(); + _systemUnderTest = new OpenFeatureBuilder(_services); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) + { + // Act + var result = useServiceProviderDelegate ? + _systemUnderTest.AddContext(_ => { }) : + _systemUnderTest.AddContext((_, _) => { }); + + // Assert + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); + _services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(EvaluationContext) && + serviceDescriptor.Lifetime == ServiceLifetime.Transient, + "A transient service of type EvaluationContext should be added."); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) + { + // Arrange + bool delegateCalled = false; + + _ = useServiceProviderDelegate ? + _systemUnderTest.AddContext(_ => delegateCalled = true) : + _systemUnderTest.AddContext((_, _) => delegateCalled = true); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var context = serviceProvider.GetService(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); + context.Should().NotBeNull("The EvaluationContext should be resolvable."); + delegateCalled.Should().BeTrue("The delegate should be invoked."); + } + +#if NET8_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif + [Fact] + public void AddProvider_ShouldAddProviderToCollection() + { + // Act + var result = _systemUnderTest.AddProvider(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(FeatureProvider) && + serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + "A singleton service of type FeatureProvider should be added."); + } + +#if NET8_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif + [Fact] + public void AddProvider_ShouldResolveCorrectProvider() + { + // Arrange + _systemUnderTest.AddProvider(); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetService(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..40e761d2 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class OpenFeatureServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _systemUnderTest; + private readonly Action _configureAction; + + public OpenFeatureServiceCollectionExtensionsTests() + { + _systemUnderTest = new ServiceCollection(); + _configureAction = Substitute.For>(); + } + + [Fact] + public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSingleton() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); + } + + [Fact] + public void AddOpenFeature_ShouldInvokeConfigureAction() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + // Assert + _configureAction.Received(1).Invoke(Arg.Any()); + } +} From 94681f37821cc44388f0cd8898924cbfbcda0cd3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:59:41 +0000 Subject: [PATCH 211/316] chore(deps): update dotnet monorepo to 8.0.2 (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Microsoft.Extensions.DependencyInjection.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `8.0.1` -> `8.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/8.0.1/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/8.0.1/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [Microsoft.Extensions.Logging.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `8.0.1` -> `8.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.1/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Logging.Abstractions/8.0.1/8.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
dotnet/runtime (Microsoft.Extensions.DependencyInjection.Abstractions) ### [`v8.0.2`](https://redirect.github.com/dotnet/runtime/releases/tag/v8.0.2): .NET 8.0.2 [Release](https://redirect.github.com/dotnet/core/releases/tag/v8.0.2)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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 these updates 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> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ab66dbb9..6db91397 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,8 +7,8 @@ - - + + From 25bc54becc4963d4994dfc9d7853dd6ee851269a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:56:08 -0500 Subject: [PATCH 212/316] chore(main): release 2.1.0 (#320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.1.0](https://github.com/open-feature/dotnet-sdk/compare/v2.0.0...v2.1.0) (2024-11-18) ### πŸ› Bug Fixes * Fix action syntax in workflow configuration ([#315](https://github.com/open-feature/dotnet-sdk/issues/315)) ([ccf0250](https://github.com/open-feature/dotnet-sdk/commit/ccf02506ecd924738b6ae03dedf25c8e2df6d1fb)) * Fix unit test clean context ([#313](https://github.com/open-feature/dotnet-sdk/issues/313)) ([3038142](https://github.com/open-feature/dotnet-sdk/commit/30381423333c54e1df98d7721dd72697fc5406dc)) ### ✨ New Features * Add Dependency Injection and Hosting support for OpenFeature ([#310](https://github.com/open-feature/dotnet-sdk/issues/310)) ([1aaa0ec](https://github.com/open-feature/dotnet-sdk/commit/1aaa0ec0e75d5048554752db30193694f0999a4a)) ### 🧹 Chore * **deps:** update actions/upload-artifact action to v4.4.3 ([#292](https://github.com/open-feature/dotnet-sdk/issues/292)) ([9b693f7](https://github.com/open-feature/dotnet-sdk/commit/9b693f737f111ed878749f725dd4c831206b308a)) * **deps:** update codecov/codecov-action action to v4.6.0 ([#306](https://github.com/open-feature/dotnet-sdk/issues/306)) ([4b92528](https://github.com/open-feature/dotnet-sdk/commit/4b92528bd56541ca3701bd4cf80467cdda80f046)) * **deps:** update dependency dotnet-sdk to v8.0.401 ([#296](https://github.com/open-feature/dotnet-sdk/issues/296)) ([0bae29d](https://github.com/open-feature/dotnet-sdk/commit/0bae29d4771c4901e0c511b8d3587e6501e67ecd)) * **deps:** update dependency fluentassertions to 6.12.2 ([#302](https://github.com/open-feature/dotnet-sdk/issues/302)) ([bc7e187](https://github.com/open-feature/dotnet-sdk/commit/bc7e187b7586a04e0feb9ef28291ce14c9ac35c5)) * **deps:** update dependency microsoft.net.test.sdk to 17.11.0 ([#297](https://github.com/open-feature/dotnet-sdk/issues/297)) ([5593e19](https://github.com/open-feature/dotnet-sdk/commit/5593e19ca990196f754cd0be69391abb8f0dbcd5)) * **deps:** update dependency microsoft.net.test.sdk to 17.11.1 ([#301](https://github.com/open-feature/dotnet-sdk/issues/301)) ([5b979d2](https://github.com/open-feature/dotnet-sdk/commit/5b979d290d96020ffe7f3e5729550d6f988b2af2)) * **deps:** update dependency nsubstitute to 5.3.0 ([#311](https://github.com/open-feature/dotnet-sdk/issues/311)) ([87f9cfa](https://github.com/open-feature/dotnet-sdk/commit/87f9cfa9b5ace84546690fea95f33bf06fd1947b)) * **deps:** update dependency xunit to 2.9.2 ([#303](https://github.com/open-feature/dotnet-sdk/issues/303)) ([2273948](https://github.com/open-feature/dotnet-sdk/commit/22739486ee107562c72d02a46190c651e59a753c)) * **deps:** update dotnet monorepo ([#305](https://github.com/open-feature/dotnet-sdk/issues/305)) ([3955b16](https://github.com/open-feature/dotnet-sdk/commit/3955b1604d5dad9b67e01974d96d53d5cacb9aad)) * **deps:** update dotnet monorepo to 8.0.2 ([#319](https://github.com/open-feature/dotnet-sdk/issues/319)) ([94681f3](https://github.com/open-feature/dotnet-sdk/commit/94681f37821cc44388f0cd8898924cbfbcda0cd3)) * update release please config ([#304](https://github.com/open-feature/dotnet-sdk/issues/304)) ([c471c06](https://github.com/open-feature/dotnet-sdk/commit/c471c062cf70d78b67f597f468c62dbfbf0674d2)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 895bf0e3..969d3dbf 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.0.0" + ".": "2.1.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fb316f5d..038e2383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [2.1.0](https://github.com/open-feature/dotnet-sdk/compare/v2.0.0...v2.1.0) (2024-11-18) + + +### πŸ› Bug Fixes + +* Fix action syntax in workflow configuration ([#315](https://github.com/open-feature/dotnet-sdk/issues/315)) ([ccf0250](https://github.com/open-feature/dotnet-sdk/commit/ccf02506ecd924738b6ae03dedf25c8e2df6d1fb)) +* Fix unit test clean context ([#313](https://github.com/open-feature/dotnet-sdk/issues/313)) ([3038142](https://github.com/open-feature/dotnet-sdk/commit/30381423333c54e1df98d7721dd72697fc5406dc)) + + +### ✨ New Features + +* Add Dependency Injection and Hosting support for OpenFeature ([#310](https://github.com/open-feature/dotnet-sdk/issues/310)) ([1aaa0ec](https://github.com/open-feature/dotnet-sdk/commit/1aaa0ec0e75d5048554752db30193694f0999a4a)) + + +### 🧹 Chore + +* **deps:** update actions/upload-artifact action to v4.4.3 ([#292](https://github.com/open-feature/dotnet-sdk/issues/292)) ([9b693f7](https://github.com/open-feature/dotnet-sdk/commit/9b693f737f111ed878749f725dd4c831206b308a)) +* **deps:** update codecov/codecov-action action to v4.6.0 ([#306](https://github.com/open-feature/dotnet-sdk/issues/306)) ([4b92528](https://github.com/open-feature/dotnet-sdk/commit/4b92528bd56541ca3701bd4cf80467cdda80f046)) +* **deps:** update dependency dotnet-sdk to v8.0.401 ([#296](https://github.com/open-feature/dotnet-sdk/issues/296)) ([0bae29d](https://github.com/open-feature/dotnet-sdk/commit/0bae29d4771c4901e0c511b8d3587e6501e67ecd)) +* **deps:** update dependency fluentassertions to 6.12.2 ([#302](https://github.com/open-feature/dotnet-sdk/issues/302)) ([bc7e187](https://github.com/open-feature/dotnet-sdk/commit/bc7e187b7586a04e0feb9ef28291ce14c9ac35c5)) +* **deps:** update dependency microsoft.net.test.sdk to 17.11.0 ([#297](https://github.com/open-feature/dotnet-sdk/issues/297)) ([5593e19](https://github.com/open-feature/dotnet-sdk/commit/5593e19ca990196f754cd0be69391abb8f0dbcd5)) +* **deps:** update dependency microsoft.net.test.sdk to 17.11.1 ([#301](https://github.com/open-feature/dotnet-sdk/issues/301)) ([5b979d2](https://github.com/open-feature/dotnet-sdk/commit/5b979d290d96020ffe7f3e5729550d6f988b2af2)) +* **deps:** update dependency nsubstitute to 5.3.0 ([#311](https://github.com/open-feature/dotnet-sdk/issues/311)) ([87f9cfa](https://github.com/open-feature/dotnet-sdk/commit/87f9cfa9b5ace84546690fea95f33bf06fd1947b)) +* **deps:** update dependency xunit to 2.9.2 ([#303](https://github.com/open-feature/dotnet-sdk/issues/303)) ([2273948](https://github.com/open-feature/dotnet-sdk/commit/22739486ee107562c72d02a46190c651e59a753c)) +* **deps:** update dotnet monorepo ([#305](https://github.com/open-feature/dotnet-sdk/issues/305)) ([3955b16](https://github.com/open-feature/dotnet-sdk/commit/3955b1604d5dad9b67e01974d96d53d5cacb9aad)) +* **deps:** update dotnet monorepo to 8.0.2 ([#319](https://github.com/open-feature/dotnet-sdk/issues/319)) ([94681f3](https://github.com/open-feature/dotnet-sdk/commit/94681f37821cc44388f0cd8898924cbfbcda0cd3)) +* update release please config ([#304](https://github.com/open-feature/dotnet-sdk/issues/304)) ([c471c06](https://github.com/open-feature/dotnet-sdk/commit/c471c062cf70d78b67f597f468c62dbfbf0674d2)) + ## [2.0.0](https://github.com/open-feature/dotnet-sdk/compare/v1.5.0...v2.0.0) (2024-08-21) Today we're announcing the release of the OpenFeature SDK for .NET, v2.0! This release contains several ergonomic improvements to the SDK, which .NET developers will appreciate. It also includes some performance optimizations brought to you by the latest .NET primitives. diff --git a/README.md b/README.md index 4f2193bf..acb31e8f 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v2.0.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.0.0) + ![Release](https://img.shields.io/static/v1?label=release&message=v2.1.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.1.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index 656f3476..21a7efd6 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.0.0 + 2.1.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 227cea21..7ec1d6db 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.0.0 +2.1.0 From bf9de4e177a4963340278854a25dd355f95dfc51 Mon Sep 17 00:00:00 2001 From: chrfwow Date: Tue, 3 Dec 2024 17:06:25 +0100 Subject: [PATCH 213/316] feat: Support Returning Error Resolutions from Providers (#323) When provider resolutions with error code set other than `None` are returned, the provider acts as if an error was thrown. Signed-off-by: christian.lutnik --- src/OpenFeature/OpenFeatureClient.cs | 18 +++++++++- .../OpenFeatureClientTests.cs | 36 +++++++++++++++++++ test/OpenFeature.Tests/TestImplementations.cs | 29 ++++++++++++--- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 08e29533..c2621785 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -263,7 +263,23 @@ private async Task> EvaluateFlagAsync( (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false)) .ToFlagEvaluationDetails(); - await this.TriggerAfterHooksAsync(allHooksReversed, hookContext, evaluation, options, cancellationToken).ConfigureAwait(false); + if (evaluation.ErrorType == ErrorType.None) + { + await this.TriggerAfterHooksAsync( + allHooksReversed, + hookContext, + evaluation, + options, + cancellationToken + ).ConfigureAwait(false); + } + else + { + var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage); + this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception); + await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, options, cancellationToken) + .ConfigureAwait(false); + } } catch (FeatureProviderException ex) { diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index ce3e9e93..ee9eee0c 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -433,6 +433,41 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } + [Fact] + public async Task When_Error_Is_Returned_From_Provider_Should_Not_Run_After_Hook_But_Error_Hook() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + const string testMessage = "Couldn't parse flag data."; + + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()) + .Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, + "ERROR", null, testMessage))); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); + var testHook = new TestHook(); + client.AddHooks(testHook); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); + + response.ErrorType.Should().Be(ErrorType.ParseError); + response.Reason.Should().Be(Reason.Error); + response.ErrorMessage.Should().Be(testMessage); + _ = featureProviderMock.Received(1) + .ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); + + Assert.Equal(1, testHook.BeforeCallCount); + Assert.Equal(0, testHook.AfterCallCount); + Assert.Equal(1, testHook.ErrorCallCount); + Assert.Equal(1, testHook.FinallyCallCount); + } + [Fact] public async Task Cancellation_Token_Added_Is_Passed_To_Provider() { @@ -454,6 +489,7 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() { await Task.Delay(10); // artificially delay until cancelled } + return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); }); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index 7a1dff10..ea35b870 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -8,28 +8,49 @@ namespace OpenFeature.Tests { - public class TestHookNoOverride : Hook { } + public class TestHookNoOverride : Hook + { + } public class TestHook : Hook { - public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + private int _beforeCallCount; + public int BeforeCallCount { get => this._beforeCallCount; } + + private int _afterCallCount; + public int AfterCallCount { get => this._afterCallCount; } + + private int _errorCallCount; + public int ErrorCallCount { get => this._errorCallCount; } + + private int _finallyCallCount; + public int FinallyCallCount { get => this._finallyCallCount; } + + public override ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { + Interlocked.Increment(ref this._beforeCallCount); return new ValueTask(EvaluationContext.Empty); } public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { + Interlocked.Increment(ref this._afterCallCount); return new ValueTask(); } - public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + public override ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { + Interlocked.Increment(ref this._errorCallCount); return new ValueTask(); } - public override ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + public override ValueTask FinallyAsync(HookContext context, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { + Interlocked.Increment(ref this._finallyCallCount); return new ValueTask(); } } From 70f847b2979e9b2b69f4e560799e2bc9fe87d5e8 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 6 Dec 2024 22:45:35 +0400 Subject: [PATCH 214/316] feat: Feature Provider Enhancements- #321 (#324) This update focuses on enhancing the feature provider integration and testing framework, incorporating improvements to flexibility, usability, and testing capabilities. This update addresses [GitHub Issue #321](https://github.com/open-feature/dotnet-sdk/issues/321) in the OpenFeature .NET SDK repository. --- ### Key Enhancements 1. **Dependency Injection (DI) Enhancements:** - Improved lifecycle management for better resource handling. - Streamlined registration for feature providers, reducing configuration complexity. - Introduced the `AddProvider` extension method to simplify and adapt feature provider integration during service setup. 2. **Simplified Codebase:** - Removed `FeatureProviderFactory` logic, eliminating unnecessary complexity and improving usability. 3. **Improved InMemoryProvider:** - Enhanced the registration process for the `InMemoryProvider`, enabling smoother and more intuitive usage. 4. **Testing Improvements:** - Established a dedicated integration test project for comprehensive validation. - Improved overall test coverage, ensuring the reliability and robustness of the framework. --------- Signed-off-by: Artyom Tonoyan Co-authored-by: chrfwow --- Directory.Packages.props | 3 +- OpenFeature.sln | 7 + README.md | 60 +++--- .../IFeatureProviderFactory.cs | 23 --- .../OpenFeatureBuilderExtensions.cs | 173 ++++++++---------- .../OpenFeatureOptions.cs | 4 +- .../OpenFeatureServiceCollectionExtensions.cs | 11 +- .../Memory/FeatureBuilderExtensions.cs | 109 +++++++++-- .../Memory/InMemoryProviderFactory.cs | 34 ---- .../Memory/InMemoryProviderOptions.cs | 19 ++ .../NoOpFeatureProviderFactory.cs | 9 - .../OpenFeatureBuilderExtensionsTests.cs | 171 +++++++++++++++-- .../FeatureFlagIntegrationTest.cs | 144 +++++++++++++++ .../OpenFeature.IntegrationTests.csproj | 31 ++++ .../Services/FeatureFlagResponse.cs | 3 + .../IFeatureFlagConfigurationService.cs | 8 + .../Services/UserInfo.cs | 3 + .../Services/UserInfoHelper.cs | 36 ++++ 18 files changed, 616 insertions(+), 232 deletions(-) delete mode 100644 src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs delete mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs create mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs delete mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs create mode 100644 test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs create mode 100644 test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj create mode 100644 test/OpenFeature.IntegrationTests/Services/FeatureFlagResponse.cs create mode 100644 test/OpenFeature.IntegrationTests/Services/IFeatureFlagConfigurationService.cs create mode 100644 test/OpenFeature.IntegrationTests/Services/UserInfo.cs create mode 100644 test/OpenFeature.IntegrationTests/Services/UserInfoHelper.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 6db91397..47330d45 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + @@ -29,7 +30,7 @@ - + diff --git a/OpenFeature.sln b/OpenFeature.sln index e8191acd..ff4cb97e 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -85,6 +85,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjec EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.IntegrationTests", "test\OpenFeature.IntegrationTests\OpenFeature.IntegrationTests.csproj", "{68463B47-36B4-8DB5-5D02-662C169E85B0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -119,6 +121,10 @@ Global {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU + {68463B47-36B4-8DB5-5D02-662C169E85B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68463B47-36B4-8DB5-5D02-662C169E85B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68463B47-36B4-8DB5-5D02-662C169E85B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68463B47-36B4-8DB5-5D02-662C169E85B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -137,6 +143,7 @@ Global {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} + {68463B47-36B4-8DB5-5D02-662C169E85B0} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/README.md b/README.md index acb31e8f..451d68e9 100644 --- a/README.md +++ b/README.md @@ -338,41 +338,43 @@ builder.Services.AddOpenFeature(featureBuilder => { }); }); ``` -#### Creating a New Provider -To integrate a custom provider, such as InMemoryProvider, you’ll need to create a factory that builds and configures the provider. This section demonstrates how to set up InMemoryProvider as a new provider with custom configuration options. -**Configuring InMemoryProvider as a New Provider** -
Begin by creating a custom factory class, `InMemoryProviderFactory`, that implements `IFeatureProviderFactory`. This factory will initialize your provider with any necessary configurations. -```csharp -public class InMemoryProviderFactory : IFeatureProviderFactory -{ - internal IDictionary? Flags { get; set; } - - public FeatureProvider Create() => new InMemoryProvider(Flags); -} -``` -**Adding an Extension Method to OpenFeatureBuilder** -
To streamline the configuration process, add an extension method, `AddInMemoryProvider`, to `OpenFeatureBuilder`. This allows you to set up the provider with either a domain-scoped or a default configuration. +### Registering a Custom Provider +You can register a custom provider, such as `InMemoryProvider`, with OpenFeature using the `AddProvider` method. This approach allows you to dynamically resolve services or configurations during registration. ```csharp -public static partial class FeatureBuilderExtensions -{ - public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null) - => builder.AddProvider(factory => ConfigureFlags(factory, configure)); +services.AddOpenFeature() + .AddProvider(provider => + { + // Resolve services or configurations as needed + var configuration = provider.GetRequiredService(); + var flags = new Dictionary + { + { "feature-key", new Flag(configuration.GetValue("FeatureFlags:Key")) } + }; + + // Register a custom provider, such as InMemoryProvider + return new InMemoryProvider(flags); + }); +``` - public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null) - => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure)); +#### Adding a Domain-Scoped Provider - private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure) - { - if (configure == null) - return; +You can also register a domain-scoped custom provider, enabling configurations specific to each domain: - var flag = new Dictionary(); - configure.Invoke(flag); - factory.Flags = flag; - } -} +```csharp +services.AddOpenFeature() + .AddProvider("my-domain", (provider, domain) => + { + // Resolve services or configurations as needed for the domain + var flags = new Dictionary + { + { $"{domain}-feature-key", new Flag(true) } + }; + + // Register a domain-scoped custom provider such as InMemoryProvider + return new InMemoryProvider(flags); + }); ``` diff --git a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs deleted file mode 100644 index 8c40cee3..00000000 --- a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OpenFeature.DependencyInjection; - -/// -/// Provides a contract for creating instances of . -/// This factory interface enables custom configuration and initialization of feature providers -/// to support domain-specific or application-specific feature flag management. -/// -#if NET8_0_OR_GREATER -[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] -#endif -public interface IFeatureProviderFactory -{ - /// - /// Creates an instance of a configured according to - /// the specific settings implemented by the concrete factory. - /// - /// - /// A new instance of . - /// The configuration and behavior of this provider instance are determined by - /// the implementation of this method. - /// - FeatureProvider Create(); -} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index a494b045..a9c3f258 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -21,9 +21,7 @@ public static partial class OpenFeatureBuilderExtensions /// the desired configuration /// The instance. /// Thrown when the or action is null. - public static OpenFeatureBuilder AddContext( - this OpenFeatureBuilder builder, - Action configure) + public static OpenFeatureBuilder AddContext(this OpenFeatureBuilder builder, Action configure) { Guard.ThrowIfNull(builder); Guard.ThrowIfNull(configure); @@ -38,9 +36,7 @@ public static OpenFeatureBuilder AddContext( /// the desired configuration /// The instance. /// Thrown when the or action is null. - public static OpenFeatureBuilder AddContext( - this OpenFeatureBuilder builder, - Action configure) + public static OpenFeatureBuilder AddContext(this OpenFeatureBuilder builder, Action configure) { Guard.ThrowIfNull(builder); Guard.ThrowIfNull(configure); @@ -57,122 +53,106 @@ public static OpenFeatureBuilder AddContext( } /// - /// Adds a new feature provider with specified options and configuration builder. + /// Adds a feature provider using a factory method without additional configuration options. + /// This method adds the feature provider as a transient service and sets it as the default provider within the application. /// - /// The type for configuring the feature provider. - /// The type of the provider factory implementing . - /// The instance. - /// An optional action to configure the provider factory of type . - /// The instance. - /// Thrown when the is null. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) + /// The used to configure feature flags. + /// + /// A factory method that creates and returns a + /// instance based on the provided service provider. + /// + /// The updated instance with the default feature provider set and configured. + /// Thrown if the is null, as a valid builder is required to add and configure providers. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory) + => AddProvider(builder, implementationFactory, null); + + /// + /// Adds a feature provider using a factory method to create the provider instance and optionally configures its settings. + /// This method adds the feature provider as a transient service and sets it as the default provider within the application. + /// + /// Type derived from used to configure the feature provider. + /// The used to configure feature flags. + /// + /// A factory method that creates and returns a + /// instance based on the provided service provider. + /// + /// An optional delegate to configure the provider-specific options. + /// The updated instance with the default feature provider set and configured. + /// Thrown if the is null, as a valid builder is required to add and configure providers. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory, Action? configureOptions) where TOptions : OpenFeatureOptions - where TProviderFactory : class, IFeatureProviderFactory { Guard.ThrowIfNull(builder); builder.HasDefaultProvider = true; - - builder.Services.Configure(options => - { - options.AddDefaultProviderName(); - }); - - if (configureFactory != null) + builder.Services.PostConfigure(options => options.AddDefaultProviderName()); + if (configureOptions != null) { - builder.Services.AddOptions() - .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") - .Configure(configureFactory); + builder.Services.Configure(configureOptions); } - else - { - builder.Services.AddOptions() - .Configure(options => { }); - } - - builder.Services.TryAddSingleton(static provider => - { - var providerFactory = provider.GetRequiredService>().Value; - return providerFactory.Create(); - }); + builder.Services.TryAddTransient(implementationFactory); builder.AddClient(); - return builder; } /// - /// Adds a new feature provider with the default type and a specified configuration builder. - /// - /// The type of the provider factory implementing . - /// The instance. - /// An optional action to configure the provider factory of type . - /// The configured instance. - /// Thrown when the is null. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) - where TProviderFactory : class, IFeatureProviderFactory - => AddProvider(builder, configureFactory); - - /// - /// Adds a feature provider with specified options and configuration builder for the specified domain. + /// Adds a feature provider for a specific domain using provided options and a configuration builder. /// - /// The type for configuring the feature provider. - /// The type of the provider factory implementing . - /// The instance. + /// Type derived from used to configure the feature provider. + /// The used to configure feature flags. /// The unique name of the provider. - /// An optional action to configure the provider factory of type . - /// The instance. - /// Thrown when the or is null or empty. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) + /// + /// A factory method that creates a feature provider instance. + /// It adds the provider as a transient service unless it is already added. + /// + /// An optional delegate to configure the provider-specific options. + /// The updated instance with the new feature provider configured. + /// + /// Thrown if either or is null or if the is empty. + /// + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory, Action? configureOptions) where TOptions : OpenFeatureOptions - where TProviderFactory : class, IFeatureProviderFactory { Guard.ThrowIfNull(builder); - Guard.ThrowIfNullOrWhiteSpace(domain, nameof(domain)); builder.DomainBoundProviderRegistrationCount++; - builder.Services.Configure(options => - { - options.AddProviderName(domain); - }); - - if (configureFactory != null) - { - builder.Services.AddOptions(domain) - .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") - .Configure(configureFactory); - } - else + builder.Services.PostConfigure(options => options.AddProviderName(domain)); + if (configureOptions != null) { - builder.Services.AddOptions(domain) - .Configure(options => { }); + builder.Services.Configure(domain, configureOptions); } - builder.Services.TryAddKeyedSingleton(domain, static (provider, key) => + builder.Services.TryAddKeyedTransient(domain, (provider, key) => { - var options = provider.GetRequiredService>(); - var providerFactory = options.Get(key!.ToString()); - return providerFactory.Create(); + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + return implementationFactory(provider, key.ToString()!); }); builder.AddClient(domain); - return builder; } /// - /// Adds a feature provider with a specified configuration builder for the specified domain, using default . + /// Adds a feature provider for a specified domain using the default options. + /// This method configures a feature provider without custom options, delegating to the more generic AddProvider method. /// - /// The type of the provider factory implementing . - /// The instance. - /// The unique domain of the provider. - /// An optional action to configure the provider factory of type . - /// The configured instance. - /// Thrown when the or is null or empty. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) - where TProviderFactory : class, IFeatureProviderFactory - => AddProvider(builder, domain, configureFactory); + /// The used to configure feature flags. + /// The unique name of the provider. + /// + /// A factory method that creates a feature provider instance. + /// It adds the provider as a transient service unless it is already added. + /// + /// The updated instance with the new feature provider configured. + /// + /// Thrown if either or is null or if the is empty. + /// + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory) + => AddProvider(builder, domain, implementationFactory, configureOptions: null); /// /// Adds a feature client to the service collection, configuring it to work with a specific context if provided. @@ -231,19 +211,24 @@ internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, st } /// - /// Configures a default client for OpenFeature using the provided factory function. + /// Adds a default to the based on the policy name options. + /// This method configures the dependency injection container to resolve the appropriate + /// depending on the policy name selected. + /// If no name is selected (i.e., null), it retrieves the default client. /// /// The instance. - /// - /// A factory function that creates an based on the service provider and . - /// /// The configured instance. - internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func clientFactory) + internal static OpenFeatureBuilder AddPolicyBasedClient(this OpenFeatureBuilder builder) { builder.Services.AddScoped(provider => { var policy = provider.GetRequiredService>().Value; - return clientFactory(provider, policy); + var name = policy.DefaultNameSelector(provider); + if (name == null) + { + return provider.GetRequiredService(); + } + return provider.GetRequiredKeyedService(name); }); return builder; diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs index 1be312ed..b2f15e44 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -26,13 +26,13 @@ public class OpenFeatureOptions /// Registers the default provider name if no specific name is provided. /// Sets to true. /// - public void AddDefaultProviderName() => AddProviderName(null); + protected internal void AddDefaultProviderName() => AddProviderName(null); /// /// Registers a new feature provider name. This operation is thread-safe. /// /// The name of the feature provider to register. Registers as default if null. - public void AddProviderName(string? name) + protected internal void AddProviderName(string? name) { if (string.IsNullOrWhiteSpace(name)) { diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index e7a503bb..74d01ad3 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -53,16 +53,7 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services }); } - builder.AddDefaultClient((provider, policy) => - { - var name = policy.DefaultNameSelector.Invoke(provider); - if (name == null) - { - return provider.GetRequiredService(); - } - return provider.GetRequiredKeyedService(name); - }); - + builder.AddPolicyBasedClient(); return services; } } diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs index 199e01b0..d6346ad7 100644 --- a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenFeature.Providers.Memory; namespace OpenFeature.DependencyInjection.Providers.Memory; @@ -10,46 +12,115 @@ namespace OpenFeature.DependencyInjection.Providers.Memory; #endif public static partial class FeatureBuilderExtensions { + /// + /// Adds an in-memory feature provider to the with a factory for flags. + /// + /// The instance to configure. + /// + /// A factory function to provide an of flags. + /// If null, an empty provider will be created. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Func?> flagsFactory) + => builder.AddProvider(provider => + { + var flags = flagsFactory(provider); + if (flags == null) + { + return new InMemoryProvider(); + } + + return new InMemoryProvider(flags); + }); + + /// + /// Adds an in-memory feature provider to the with a domain and factory for flags. + /// + /// The instance to configure. + /// The unique domain of the provider. + /// + /// A factory function to provide an of flags. + /// If null, an empty provider will be created. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Func?> flagsFactory) + => AddInMemoryProvider(builder, domain, (provider, _) => flagsFactory(provider)); + + /// + /// Adds an in-memory feature provider to the with a domain and contextual flag factory. + /// If null, an empty provider will be created. + /// + /// The instance to configure. + /// The unique domain of the provider. + /// + /// A factory function to provide an of flags based on service provider and domain. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Func?> flagsFactory) + => builder.AddProvider(domain, (provider, key) => + { + var flags = flagsFactory(provider, key); + if (flags == null) + { + return new InMemoryProvider(); + } + + return new InMemoryProvider(flags); + }); + /// /// Adds an in-memory feature provider to the with optional flag configuration. /// /// The instance to configure. /// /// An optional delegate to configure feature flags in the in-memory provider. - /// If provided, it allows setting up the initial flags. + /// If null, an empty provider will be created. /// /// The instance for chaining. public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null) - => builder.AddProvider(factory => ConfigureFlags(factory, configure)); + => builder.AddProvider(CreateProvider, options => ConfigureFlags(options, configure)); /// - /// Adds an in-memory feature provider with a specific domain to the - /// with optional flag configuration. + /// Adds an in-memory feature provider with a specific domain to the with optional flag configuration. /// /// The instance to configure. /// The unique domain of the provider /// /// An optional delegate to configure feature flags in the in-memory provider. - /// If provided, it allows setting up the initial flags. + /// If null, an empty provider will be created. /// /// The instance for chaining. public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null) - => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure)); + => builder.AddProvider(domain, CreateProvider, options => ConfigureFlags(options, configure)); - /// - /// Configures the feature flags for an instance. - /// - /// The to configure. - /// - /// An optional delegate that sets up the initial flags in the provider's flag dictionary. - /// - private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure) + private static FeatureProvider CreateProvider(IServiceProvider provider, string domain) { - if (configure == null) - return; + var options = provider.GetRequiredService>().Get(domain); + if (options.Flags == null) + { + return new InMemoryProvider(); + } - var flag = new Dictionary(); - configure.Invoke(flag); - factory.Flags = flag; + return new InMemoryProvider(options.Flags); + } + + private static FeatureProvider CreateProvider(IServiceProvider provider) + { + var options = provider.GetRequiredService>().Value; + if (options.Flags == null) + { + return new InMemoryProvider(); + } + + return new InMemoryProvider(options.Flags); + } + + private static void ConfigureFlags(InMemoryProviderOptions options, Action>? configure) + { + if (configure != null) + { + options.Flags = new Dictionary(); + configure.Invoke(options.Flags); + } } } diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs deleted file mode 100644 index 2d155dd9..00000000 --- a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using OpenFeature.Providers.Memory; - -namespace OpenFeature.DependencyInjection.Providers.Memory; - -/// -/// A factory for creating instances of , -/// an in-memory implementation of . -/// This factory allows for the customization of feature flags to facilitate -/// testing and lightweight feature flag management without external dependencies. -/// -#if NET8_0_OR_GREATER -[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] -#endif -public class InMemoryProviderFactory : IFeatureProviderFactory -{ - /// - /// Gets or sets the collection of feature flags used to configure the - /// instances. This dictionary maps - /// flag names to instances, enabling pre-configuration - /// of features for testing or in-memory evaluation. - /// - internal IDictionary? Flags { get; set; } - - /// - /// Creates a new instance of with the specified - /// flags set in . This instance is configured for in-memory - /// feature flag management, suitable for testing or lightweight feature toggling scenarios. - /// - /// - /// A configured that can be used to manage - /// feature flags in an in-memory context. - /// - public FeatureProvider Create() => new InMemoryProvider(Flags); -} diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs new file mode 100644 index 00000000..ea5433f4 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs @@ -0,0 +1,19 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.DependencyInjection.Providers.Memory; + +/// +/// Options for configuring the in-memory feature flag provider. +/// +public class InMemoryProviderOptions : OpenFeatureOptions +{ + /// + /// Gets or sets the feature flags to be used by the in-memory provider. + /// + /// + /// This property allows you to specify a dictionary of flags where the key is the flag name + /// and the value is the corresponding instance. + /// If no flags are provided, the in-memory provider will start with an empty set of flags. + /// + public IDictionary? Flags { get; set; } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs deleted file mode 100644 index 1ee14bf0..00000000 --- a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OpenFeature.DependencyInjection.Tests; - -#if NET8_0_OR_GREATER -[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] -#endif -public class NoOpFeatureProviderFactory : IFeatureProviderFactory -{ - public FeatureProvider Create() => new NoOpFeatureProvider(); -} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 3f6ef227..087336a0 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenFeature.Model; using Xunit; @@ -22,12 +23,12 @@ public OpenFeatureBuilderExtensionsTests() public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) { // Act - var result = useServiceProviderDelegate ? + var featureBuilder = useServiceProviderDelegate ? _systemUnderTest.AddContext(_ => { }) : _systemUnderTest.AddContext((_, _) => { }); // Assert - result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(EvaluationContext) && @@ -61,37 +62,185 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] #endif - [Fact] - public void AddProvider_ShouldAddProviderToCollection() + [Theory] + [InlineData(1, true, 0)] + [InlineData(2, false, 1)] + [InlineData(3, true, 0)] + [InlineData(4, false, 1)] + public void AddProvider_ShouldAddProviderToCollection(int providerRegistrationType, bool expectsDefaultProvider, int expectsDomainBoundProvider) { // Act - var result = _systemUnderTest.AddProvider(); + var featureBuilder = providerRegistrationType switch + { + 1 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()), + 2 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider()), + 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { }), + 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }), + _ => throw new InvalidOperationException("Invalid mode.") + }; // Assert _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); - result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _systemUnderTest.HasDefaultProvider.Should().Be(expectsDefaultProvider, "The default provider flag should be set correctly."); + _systemUnderTest.IsPolicyConfigured.Should().BeFalse("The policy should not be configured."); + _systemUnderTest.DomainBoundProviderRegistrationCount.Should().Be(expectsDomainBoundProvider, "The domain-bound provider count should be correct."); + featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(FeatureProvider) && - serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + serviceDescriptor.Lifetime == ServiceLifetime.Transient, "A singleton service of type FeatureProvider should be added."); } + class TestOptions : OpenFeatureOptions { } + #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] #endif - [Fact] - public void AddProvider_ShouldResolveCorrectProvider() + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void AddProvider_ShouldResolveCorrectProvider(int providerRegistrationType) { // Arrange - _systemUnderTest.AddProvider(); + _ = providerRegistrationType switch + { + 1 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()), + 2 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider()), + 3 => _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider(), o => { }), + 4 => _systemUnderTest.AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }), + _ => throw new InvalidOperationException("Invalid mode.") + }; var serviceProvider = _services.BuildServiceProvider(); // Act - var provider = serviceProvider.GetService(); + var provider = providerRegistrationType switch + { + 1 or 3 => serviceProvider.GetService(), + 2 or 4 => serviceProvider.GetKeyedService("test"), + _ => throw new InvalidOperationException("Invalid mode.") + }; + + // Assert + provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + } + + [Theory] + [InlineData(1, true, 1)] + [InlineData(2, true, 1)] + [InlineData(3, false, 2)] + [InlineData(4, true, 1)] + [InlineData(5, true, 1)] + [InlineData(6, false, 2)] + [InlineData(7, true, 2)] + [InlineData(8, true, 2)] + public void AddProvider_VerifiesDefaultAndDomainBoundProvidersBasedOnConfiguration(int providerRegistrationType, bool expectsDefaultProvider, int expectsDomainBoundProvider) + { + // Act + var featureBuilder = providerRegistrationType switch + { + 1 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider()) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()), + 2 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider()) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()), + 3 => _systemUnderTest + .AddProvider("test1", (_, _) => new NoOpFeatureProvider()) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider()), + 4 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()), + 5 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()), + 6 => _systemUnderTest + .AddProvider("test1", (_, _) => new NoOpFeatureProvider(), o => { }) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider()), + 7 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider()) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider()), + 8 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider(), o => { }), + _ => throw new InvalidOperationException("Invalid mode.") + }; // Assert _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + _systemUnderTest.HasDefaultProvider.Should().Be(expectsDefaultProvider, "The default provider flag should be set correctly."); + _systemUnderTest.IsPolicyConfigured.Should().BeFalse("The policy should not be configured."); + _systemUnderTest.DomainBoundProviderRegistrationCount.Should().Be(expectsDomainBoundProvider, "The domain-bound provider count should be correct."); + featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + } + + [Theory] + [InlineData(1, null)] + [InlineData(2, "test")] + [InlineData(3, "test2")] + [InlineData(4, "test")] + [InlineData(5, null)] + [InlineData(6, "test1")] + [InlineData(7, "test2")] + [InlineData(8, null)] + public void AddProvider_ConfiguresPolicyNameAcrossMultipleProviderSetups(int providerRegistrationType, string? policyName) + { + // Arrange + var featureBuilder = providerRegistrationType switch + { + 1 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider()) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + 2 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider()) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + 3 => _systemUnderTest + .AddProvider("test1", (_, _) => new NoOpFeatureProvider()) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider()) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + 4 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + 5 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + 6 => _systemUnderTest + .AddProvider("test1", (_, _) => new NoOpFeatureProvider(), o => { }) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider()) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + 7 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider()) + .AddProvider("test", (_, _) => new NoOpFeatureProvider()) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider()) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + 8 => _systemUnderTest + .AddProvider(_ => new NoOpFeatureProvider(), o => { }) + .AddProvider("test", (_, _) => new NoOpFeatureProvider(), o => { }) + .AddProvider("test2", (_, _) => new NoOpFeatureProvider(), o => { }) + .AddPolicyName(policy => policy.DefaultNameSelector = provider => policyName), + _ => throw new InvalidOperationException("Invalid mode.") + }; + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var policy = serviceProvider.GetRequiredService>().Value; + var name = policy.DefaultNameSelector(serviceProvider); + var provider = name == null ? + serviceProvider.GetService() : + serviceProvider.GetRequiredKeyedService(name); + + // Assert + featureBuilder.IsPolicyConfigured.Should().BeTrue("The policy should be configured."); provider.Should().NotBeNull("The FeatureProvider should be resolvable."); provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); } diff --git a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs new file mode 100644 index 00000000..559bf4bb --- /dev/null +++ b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs @@ -0,0 +1,144 @@ +using System.Net.Http.Json; +using System.Text.Json; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.DependencyInjection.Providers.Memory; +using OpenFeature.IntegrationTests.Services; +using OpenFeature.Providers.Memory; + +namespace OpenFeature.IntegrationTests; + +public class FeatureFlagIntegrationTest +{ + // TestUserId is "off", other users are "on" + private const string FeatureA = "feature-a"; + private const string TestUserId = "123"; + + [Theory] + [InlineData(TestUserId, false, ServiceLifetime.Singleton)] + [InlineData(TestUserId, false, ServiceLifetime.Scoped)] + [InlineData(TestUserId, false, ServiceLifetime.Transient)] + [InlineData("SomeOtherId", true, ServiceLifetime.Singleton)] + [InlineData("SomeOtherId", true, ServiceLifetime.Scoped)] + [InlineData("SomeOtherId", true, ServiceLifetime.Transient)] + public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string userId, bool expectedResult, ServiceLifetime serviceLifetime) + { + // Arrange + using var server = await CreateServerAsync(services => + { + switch (serviceLifetime) + { + case ServiceLifetime.Singleton: + services.AddSingleton(); + break; + case ServiceLifetime.Scoped: + services.AddScoped(); + break; + case ServiceLifetime.Transient: + services.AddTransient(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null); + } + }).ConfigureAwait(true); + + var client = server.CreateClient(); + var requestUri = $"/features/{userId}/flags/{FeatureA}"; + + // Act + var response = await client.GetAsync(requestUri).ConfigureAwait(true); + var responseContent = await response.Content.ReadFromJsonAsync>().ConfigureAwait(true); ; + + // Assert + response.IsSuccessStatusCode.Should().BeTrue("Expected HTTP status code 200 OK."); + responseContent.Should().NotBeNull("Expected response content to be non-null."); + responseContent!.FeatureName.Should().Be(FeatureA, "Expected feature name to be 'feature-a'."); + responseContent.FeatureValue.Should().Be(expectedResult, "Expected feature value to match the expected result."); + } + + private static async Task CreateServerAsync(Action? configureServices = null) + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + configureServices?.Invoke(builder.Services); + builder.Services.TryAddSingleton(); + + builder.Services.AddHttpContextAccessor(); + builder.Services.AddOpenFeature(cfg => + { + cfg.AddHostedFeatureLifecycle(); + cfg.AddContext((builder, provider) => + { + // Retrieve the HttpContext from IHttpContextAccessor, ensuring it's not null. + var context = provider.GetRequiredService().HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + var userId = UserInfoHelper.GetUserId(context); + builder.Set("user", userId); + }); + cfg.AddInMemoryProvider(provider => + { + var flagService = provider.GetRequiredService(); + return flagService.GetFlags(); + }); + }); + + var app = builder.Build(); + + app.UseRouting(); + app.Map($"/features/{{userId}}/flags/{{featureName}}", async context => + { + var client = context.RequestServices.GetRequiredService(); + var featureName = UserInfoHelper.GetFeatureName(context); + var res = await client.GetBooleanValueAsync(featureName, false).ConfigureAwait(true); + var result = await client.GetBooleanValueAsync(featureName, false).ConfigureAwait(true); + + var response = new FeatureFlagResponse(featureName, result); + + // Serialize the response object to JSON + var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + // Write the JSON response + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(jsonResponse).ConfigureAwait(true); + }); + + await app.StartAsync().ConfigureAwait(true); + + return app.GetTestServer(); + } + + public class FlagConfigurationService : IFeatureFlagConfigurationService + { + private readonly IDictionary _flags; + public FlagConfigurationService() + { + _flags = new Dictionary + { + { + "feature-a", new Flag( + variants: new Dictionary() + { + { "on", true }, + { "off", false } + }, + defaultVariant: "on", context => { + var id = context.GetValue("user").AsString; + if(id == null) + { + return "on"; // default variant + } + + return id == TestUserId ? "off" : "on"; + }) + } + }; + } + public Dictionary GetFlags() => _flags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } +} diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj new file mode 100644 index 00000000..8287b2ec --- /dev/null +++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenFeature.IntegrationTests/Services/FeatureFlagResponse.cs b/test/OpenFeature.IntegrationTests/Services/FeatureFlagResponse.cs new file mode 100644 index 00000000..50285cc0 --- /dev/null +++ b/test/OpenFeature.IntegrationTests/Services/FeatureFlagResponse.cs @@ -0,0 +1,3 @@ +namespace OpenFeature.IntegrationTests.Services; + +public record FeatureFlagResponse(string FeatureName, T FeatureValue) where T : notnull; diff --git a/test/OpenFeature.IntegrationTests/Services/IFeatureFlagConfigurationService.cs b/test/OpenFeature.IntegrationTests/Services/IFeatureFlagConfigurationService.cs new file mode 100644 index 00000000..1b51a60a --- /dev/null +++ b/test/OpenFeature.IntegrationTests/Services/IFeatureFlagConfigurationService.cs @@ -0,0 +1,8 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.IntegrationTests.Services; + +internal interface IFeatureFlagConfigurationService +{ + Dictionary GetFlags(); +} diff --git a/test/OpenFeature.IntegrationTests/Services/UserInfo.cs b/test/OpenFeature.IntegrationTests/Services/UserInfo.cs new file mode 100644 index 00000000..c2c5d8c1 --- /dev/null +++ b/test/OpenFeature.IntegrationTests/Services/UserInfo.cs @@ -0,0 +1,3 @@ +namespace OpenFeature.IntegrationTests.Services; + +public record UserInfo(string UserId, string FeatureName); diff --git a/test/OpenFeature.IntegrationTests/Services/UserInfoHelper.cs b/test/OpenFeature.IntegrationTests/Services/UserInfoHelper.cs new file mode 100644 index 00000000..0e057a6b --- /dev/null +++ b/test/OpenFeature.IntegrationTests/Services/UserInfoHelper.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; + +namespace OpenFeature.IntegrationTests.Services; + +public static class UserInfoHelper +{ + /// + /// Extracts the user ID from the HTTP request context. + /// + /// The HTTP context containing the request. + /// The user ID as a string. + /// Thrown if the user ID is not found in the route values. + public static string GetUserId(HttpContext context) + { + if (context.Request.RouteValues.TryGetValue("userId", out var userId) && userId is string userIdString) + { + return userIdString; + } + throw new ArgumentNullException(nameof(userId), "User ID not found in route values."); + } + + /// + /// Extracts the feature name from the HTTP request context. + /// + /// The HTTP context containing the request. + /// The feature name as a string. + /// Thrown if the feature name is not found in the route values. + public static string GetFeatureName(HttpContext context) + { + if (context.Request.RouteValues.TryGetValue("featureName", out var featureName) && featureName is string featureNameString) + { + return featureNameString; + } + throw new ArgumentNullException(nameof(featureName), "Feature name not found in route values."); + } +} From cbf4f25a4365eac15e37987d2d7163cb1aefacfe Mon Sep 17 00:00:00 2001 From: chrfwow Date: Wed, 11 Dec 2024 18:31:42 +0100 Subject: [PATCH 215/316] feat: Implement Tracking in .NET #309 (#327) ## This PR Adds support for tracking ### Related Issues Closes #309 --------- Signed-off-by: christian.lutnik --- README.md | 16 +- src/OpenFeature/FeatureProvider.cs | 12 ++ src/OpenFeature/Model/TrackingEventDetails.cs | 112 ++++++++++++ .../Model/TrackingEventDetailsBuilder.cs | 159 ++++++++++++++++++ src/OpenFeature/OpenFeatureClient.cs | 25 +++ .../OpenFeatureClientTests.cs | 116 +++++++++++++ test/OpenFeature.Tests/TestImplementations.cs | 16 ++ .../TrackingEventDetailsTest.cs | 50 ++++++ 8 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 src/OpenFeature/Model/TrackingEventDetails.cs create mode 100644 src/OpenFeature/Model/TrackingEventDetailsBuilder.cs create mode 100644 test/OpenFeature.Tests/TrackingEventDetailsTest.cs diff --git a/README.md b/README.md index 451d68e9..1d87c74a 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ public async Task Example() | βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | | βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | | βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | | βœ… | [Logging](#logging) | Integrate with popular logging packages. | | βœ… | [Domains](#domains) | Logically bind clients with providers. | | βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | @@ -212,6 +213,19 @@ await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider); myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); ``` +### Tracking + +The [tracking API](https://openfeature.dev/specification/sections/tracking) allows you to use OpenFeature abstractions and objects to associate user actions with feature flag evaluations. +This is essential for robust experimentation powered by feature flags. +For example, a flag enhancing the appearance of a UI component might drive user engagement to a new feature; to test this hypothesis, telemetry collected by a hook(#hooks) or provider(#providers) can be associated with telemetry reported in the client's `track` function. + +```csharp +var client = Api.Instance.GetClient(); +client.Track("visited-promo-page", trackingEventDetails: new TrackingEventDetailsBuilder().SetValue(99.77).Set("currency", "USD").Build()); +``` + +Note that some providers may not support tracking; check the documentation for your provider for more information. + ### Shutdown The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. @@ -320,7 +334,7 @@ builder.Services.AddOpenFeature(featureBuilder => { featureBuilder .AddHostedFeatureLifecycle() // From Hosting package .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ }) - .AddInMemoryProvider(); + .AddInMemoryProvider(); }); ``` **Domain-Scoped Provider Configuration:** diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index c4ce8783..de3f2797 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -7,6 +7,7 @@ using OpenFeature.Model; [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // required to allow NSubstitute mocking of internal methods + namespace OpenFeature { /// @@ -140,5 +141,16 @@ public virtual Task ShutdownAsync(CancellationToken cancellationToken = default) /// /// The event channel of the provider public virtual 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. + } } } diff --git a/src/OpenFeature/Model/TrackingEventDetails.cs b/src/OpenFeature/Model/TrackingEventDetails.cs new file mode 100644 index 00000000..0d342cc1 --- /dev/null +++ b/src/OpenFeature/Model/TrackingEventDetails.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace OpenFeature.Model; + +/// +/// The `tracking event details` structure defines optional data pertinent to a particular `tracking event`. +/// +/// +public sealed class TrackingEventDetails +{ + /// + ///A predefined value field for the tracking details. + /// + public readonly double? Value; + + private readonly Structure _structure; + + /// + /// Internal constructor used by the builder. + /// + /// + /// + internal TrackingEventDetails(Structure content, double? value) + { + this.Value = value; + this._structure = content; + } + + + /// + /// Private constructor for making an empty . + /// + private TrackingEventDetails() + { + this._structure = Structure.Empty; + this.Value = null; + } + + /// + /// Empty tracking event details. + /// + public static TrackingEventDetails Empty { get; } = new(); + + + /// + /// 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(); + } + + /// + /// 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() + { + return this._structure.GetEnumerator(); + } + + /// + /// Get a builder which can build an . + /// + /// The builder + public static TrackingEventDetailsBuilder Builder() + { + return new TrackingEventDetailsBuilder(); + } +} diff --git a/src/OpenFeature/Model/TrackingEventDetailsBuilder.cs b/src/OpenFeature/Model/TrackingEventDetailsBuilder.cs new file mode 100644 index 00000000..99a9d677 --- /dev/null +++ b/src/OpenFeature/Model/TrackingEventDetailsBuilder.cs @@ -0,0 +1,159 @@ +using System; + +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 TrackingEventDetailsBuilder + { + private readonly StructureBuilder _attributes = Structure.Builder(); + private double? _value; + + /// + /// Internal to only allow direct creation by . + /// + internal TrackingEventDetailsBuilder() { } + + /// + /// Set the predefined value field for the tracking details. + /// + /// + /// + public TrackingEventDetailsBuilder SetValue(double? value) + { + this._value = value; + return this; + } + + /// + /// Set the key to the given . + /// + /// The key for the value + /// The value to set + /// This builder + public TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder Set(string key, DateTime value) + { + this._attributes.Set(key, value); + return this; + } + + /// + /// Incorporate existing tracking details into the builder. + /// + /// Any existing keys in the builder will be replaced by keys in the tracking details, including the Value set + /// through . + /// + /// + /// The tracking details to add merge + /// This builder + public TrackingEventDetailsBuilder Merge(TrackingEventDetails trackingDetails) + { + this._value = trackingDetails.Value; + foreach (var kvp in trackingDetails) + { + this.Set(kvp.Key, kvp.Value); + } + + return this; + } + + /// + /// Build an immutable . + /// + /// An immutable + public TrackingEventDetails Build() + { + return new TrackingEventDetails(this._attributes.Build(), this._value); + } + } +} diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index c2621785..e774c6b5 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -367,6 +367,31 @@ private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, HookCo } } + /// + /// Use this method to track user interactions and the application state. + /// + /// 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) + /// When trackingEventName is null or empty + public void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default) + { + if (string.IsNullOrWhiteSpace(trackingEventName)) + { + throw new ArgumentException("Tracking event cannot be null or empty.", nameof(trackingEventName)); + } + + var globalContext = Api.Instance.GetContext(); + var clientContext = this.GetContext(); + + var evaluationContextBuilder = EvaluationContext.Builder() + .Merge(globalContext) + .Merge(clientContext); + if (evaluationContext != null) evaluationContextBuilder.Merge(evaluationContext); + + this._providerAccessor.Invoke().Track(trackingEventName, evaluationContextBuilder.Build(), trackingEventDetails); + } + [LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")] partial void HookReturnedNull(string hookName); diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index ee9eee0c..13d3fa93 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -540,5 +540,121 @@ public void ToFlagEvaluationDetails_Should_Convert_All_Properties() result.Should().BeEquivalentTo(expected); } + + [Fact] + [Specification("6.1.1", "The `client` MUST define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required), `evaluation context` (optional) and `tracking event details` (optional), which returns nothing.")] + [Specification("6.1.2", "The `client` MUST define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required) and `tracking event details` (optional), which returns nothing.")] + [Specification("6.1.4", "If the client's `track` function is called and the associated provider does not implement tracking, the client's `track` function MUST no-op.")] + public async Task TheClient_ImplementsATrackingFunction() + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); + + const string trackingEventName = "trackingEventName"; + var trackingEventDetails = new TrackingEventDetailsBuilder().Build(); + client.Track(trackingEventName); + client.Track(trackingEventName, EvaluationContext.Empty); + client.Track(trackingEventName, EvaluationContext.Empty, trackingEventDetails); + client.Track(trackingEventName, trackingEventDetails: trackingEventDetails); + + Assert.Equal(4, provider.GetTrackingInvocations().Count); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[0].Item1); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[1].Item1); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[2].Item1); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[3].Item1); + + Assert.NotNull(provider.GetTrackingInvocations()[0].Item2); + Assert.NotNull(provider.GetTrackingInvocations()[1].Item2); + Assert.NotNull(provider.GetTrackingInvocations()[2].Item2); + Assert.NotNull(provider.GetTrackingInvocations()[3].Item2); + + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[0].Item2!.Count); + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[1].Item2!.Count); + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[2].Item2!.Count); + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[3].Item2!.Count); + + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[0].Item2!.TargetingKey); + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[1].Item2!.TargetingKey); + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[2].Item2!.TargetingKey); + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[3].Item2!.TargetingKey); + + Assert.Null(provider.GetTrackingInvocations()[0].Item3); + Assert.Null(provider.GetTrackingInvocations()[1].Item3); + Assert.Equal(trackingEventDetails, provider.GetTrackingInvocations()[2].Item3); + Assert.Equal(trackingEventDetails, provider.GetTrackingInvocations()[3].Item3); + } + + [Fact] + public async Task PassingAnEmptyStringAsTrackingEventName_ThrowsArgumentException() + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); + + Assert.Throws(() => client.Track("")); + } + + [Fact] + public async Task PassingABlankStringAsTrackingEventName_ThrowsArgumentException() + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); + + Assert.Throws(() => client.Track(" \n ")); + } + + public static TheoryData GenerateMergeEvaluationContextTestData() + { + const string key = "key"; + const string global = "global"; + const string client = "client"; + const string invocation = "invocation"; + var globalEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "global").Build(), null }; + var clientEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "client").Build(), null }; + var invocationEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "invocation").Build(), null }; + + var data = new TheoryData(); + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 2; j++) + { + for (int k = 0; k < 2; k++) + { + if (i == 1 && j == 1 && k == 1) continue; + string expected; + if (k == 0) expected = invocation; + else if (j == 0) expected = client; + else expected = global; + data.Add(key, globalEvaluationContext[i], clientEvaluationContext[j], invocationEvaluationContext[k], expected); + } + } + } + + return data; + } + + [Theory] + [MemberData(nameof(GenerateMergeEvaluationContextTestData))] + [Specification("6.1.3", "The evaluation context passed to the provider's track function MUST be merged in the order: API (global; lowest precedence) - transaction - client - invocation (highest precedence), with duplicate values being overwritten.")] + public async Task TheClient_MergesTheEvaluationContextInTheCorrectOrder(string key, EvaluationContext? globalEvaluationContext, EvaluationContext? clientEvaluationContext, EvaluationContext? invocationEvaluationContext, string expectedResult) + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); + + const string trackingEventName = "trackingEventName"; + + Api.Instance.SetContext(globalEvaluationContext); + client.SetContext(clientEvaluationContext); + client.Track(trackingEventName, invocationEvaluationContext); + Assert.Single(provider.GetTrackingInvocations()); + var actualEvaluationContext = provider.GetTrackingInvocations()[0].Item2; + Assert.NotNull(actualEvaluationContext); + Assert.NotEqual(0, actualEvaluationContext.Count); + + Assert.Equal(expectedResult, actualEvaluationContext.GetValue(key).AsString); + } } } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index ea35b870..aa4dc784 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -60,6 +60,7 @@ public class TestProvider : FeatureProvider private readonly List _hooks = new List(); public static string DefaultName = "test-provider"; + private readonly List> TrackingInvocations = []; public string? Name { get; set; } @@ -87,6 +88,16 @@ public TestProvider(string? name, Exception? initException = null, int initDelay this.initDelay = initDelay; } + public ImmutableList> GetTrackingInvocations() + { + return this.TrackingInvocations.ToImmutableList(); + } + + public void Reset() + { + this.TrackingInvocations.Clear(); + } + public override Metadata GetMetadata() { return new Metadata(this.Name); @@ -131,6 +142,11 @@ public override async Task InitializeAsync(EvaluationContext context, Cancellati } } + public override void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default) + { + this.TrackingInvocations.Add(new Tuple(trackingEventName, evaluationContext, trackingEventDetails)); + } + internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) { return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name, }, cancellationToken); diff --git a/test/OpenFeature.Tests/TrackingEventDetailsTest.cs b/test/OpenFeature.Tests/TrackingEventDetailsTest.cs new file mode 100644 index 00000000..22b1ce45 --- /dev/null +++ b/test/OpenFeature.Tests/TrackingEventDetailsTest.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; +using Xunit; + +namespace OpenFeature.Tests; + +public class TrackingEventDetailsTest +{ + [Fact] + [Specification("6.2.1", "The `tracking event details` structure MUST define an optional numeric `value`, associating a scalar quality with an `tracking event`.")] + public void TrackingEventDetails_HasAnOptionalValueProperty() + { + var builder = new TrackingEventDetailsBuilder(); + var details = builder.Build(); + Assert.Null(details.Value); + } + + [Fact] + [Specification("6.2.1", "The `tracking event details` structure MUST define an optional numeric `value`, associating a scalar quality with an `tracking event`.")] + public void TrackingEventDetails_HasAValueProperty() + { + const double value = 23.5; + var builder = new TrackingEventDetailsBuilder().SetValue(value); + var details = builder.Build(); + Assert.Equal(value, details.Value); + } + + [Fact] + [Specification("6.2.2", "The `tracking event details` MUST support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | structure`.")] + public void TrackingEventDetails_CanTakeValues() + { + var structure = new Structure(new Dictionary { { "key", new Value("value") } }); + var dateTimeValue = new Value(DateTime.Now); + var builder = TrackingEventDetails.Builder() + .Set("boolean", true) + .Set("string", "some string") + .Set("double", 123.3) + .Set("structure", structure) + .Set("value", dateTimeValue); + var details = builder.Build(); + Assert.Equal(5, details.Count); + Assert.Equal(true, details.GetValue("boolean").AsBoolean); + Assert.Equal("some string", details.GetValue("string").AsString); + Assert.Equal(123.3, details.GetValue("double").AsDouble); + Assert.Equal(structure, details.GetValue("structure").AsStructure); + Assert.Equal(dateTimeValue, details.GetValue("value")); + } +} From 35cd77b59dc938301e7e22ddefd9b39ef8e21a4b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:35:35 -0500 Subject: [PATCH 216/316] chore(deps): update dependency fluentassertions to v7 (#325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [FluentAssertions](https://www.fluentassertions.com/) ([source](https://redirect.github.com/fluentassertions/fluentassertions)) | `6.12.2` -> `7.0.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/FluentAssertions/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/FluentAssertions/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/FluentAssertions/6.12.2/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/FluentAssertions/6.12.2/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
fluentassertions/fluentassertions (FluentAssertions) ### [`v7.0.0`](https://redirect.github.com/fluentassertions/fluentassertions/releases/tag/7.0.0) [Compare Source](https://redirect.github.com/fluentassertions/fluentassertions/compare/6.12.2...7.0.0) #### What's Changed ##### Breaking Changes - Drop support for .NET Core 2.1, 3.0 and NSpec by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2835](https://redirect.github.com/fluentassertions/fluentassertions/pull/2835) ##### Fixes - The expectation node identified as a cyclic reference is still compared to the subject node using simple equality. by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2819](https://redirect.github.com/fluentassertions/fluentassertions/pull/2819) - Fix support for write-only properties in BeEquivalentTo by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2836](https://redirect.github.com/fluentassertions/fluentassertions/pull/2836) ##### Documentation - Fix minor syntax error in objectgraphs.md by [@​rklec](https://redirect.github.com/rklec) in [https://github.com/fluentassertions/fluentassertions/pull/2847](https://redirect.github.com/fluentassertions/fluentassertions/pull/2847) ##### Others - Use the same Qodana build pipeline as develop is using by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2809](https://redirect.github.com/fluentassertions/fluentassertions/pull/2809) - Add section highlighting for better navigation by [@​sentemon](https://redirect.github.com/sentemon) in [https://github.com/fluentassertions/fluentassertions/pull/2807](https://redirect.github.com/fluentassertions/fluentassertions/pull/2807) - Bump all relevant dependencies by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2834](https://redirect.github.com/fluentassertions/fluentassertions/pull/2834) - Changed references to the master branch to main by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2848](https://redirect.github.com/fluentassertions/fluentassertions/pull/2848) - Missed two more references to master by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2849](https://redirect.github.com/fluentassertions/fluentassertions/pull/2849) - Backport bump of `System.Configuration.ConfigurationManager` and `System.Threading.Tasks.Extensions` by [@​jnyrup](https://redirect.github.com/jnyrup) in [https://github.com/fluentassertions/fluentassertions/pull/2856](https://redirect.github.com/fluentassertions/fluentassertions/pull/2856) #### New Contributors - [@​sentemon](https://redirect.github.com/sentemon) made their first contribution in [https://github.com/fluentassertions/fluentassertions/pull/2807](https://redirect.github.com/fluentassertions/fluentassertions/pull/2807) - [@​rklec](https://redirect.github.com/rklec) made their first contribution in [https://github.com/fluentassertions/fluentassertions/pull/2847](https://redirect.github.com/fluentassertions/fluentassertions/pull/2847) **Full Changelog**: https://github.com/fluentassertions/fluentassertions/compare/6.12.2...7.0.0
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 47330d45..c9995fc8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + From 1c60908bae91f0edc3b92468beab5dc031ac13fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:52:36 -0500 Subject: [PATCH 217/316] chore(main): release 2.2.0 (#326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.2.0](https://github.com/open-feature/dotnet-sdk/compare/v2.1.0...v2.2.0) (2024-12-12) ### ✨ New Features * Feature Provider Enhancements- [#321](https://github.com/open-feature/dotnet-sdk/issues/321) ([#324](https://github.com/open-feature/dotnet-sdk/issues/324)) ([70f847b](https://github.com/open-feature/dotnet-sdk/commit/70f847b2979e9b2b69f4e560799e2bc9fe87d5e8)) * Implement Tracking in .NET [#309](https://github.com/open-feature/dotnet-sdk/issues/309) ([#327](https://github.com/open-feature/dotnet-sdk/issues/327)) ([cbf4f25](https://github.com/open-feature/dotnet-sdk/commit/cbf4f25a4365eac15e37987d2d7163cb1aefacfe)) * Support Returning Error Resolutions from Providers ([#323](https://github.com/open-feature/dotnet-sdk/issues/323)) ([bf9de4e](https://github.com/open-feature/dotnet-sdk/commit/bf9de4e177a4963340278854a25dd355f95dfc51)) ### 🧹 Chore * **deps:** update dependency fluentassertions to v7 ([#325](https://github.com/open-feature/dotnet-sdk/issues/325)) ([35cd77b](https://github.com/open-feature/dotnet-sdk/commit/35cd77b59dc938301e7e22ddefd9b39ef8e21a4b)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 969d3dbf..a5d1cf28 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.1.0" + ".": "2.2.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 038e2383..36a460be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [2.2.0](https://github.com/open-feature/dotnet-sdk/compare/v2.1.0...v2.2.0) (2024-12-12) + + +### ✨ New Features + +* Feature Provider Enhancements- [#321](https://github.com/open-feature/dotnet-sdk/issues/321) ([#324](https://github.com/open-feature/dotnet-sdk/issues/324)) ([70f847b](https://github.com/open-feature/dotnet-sdk/commit/70f847b2979e9b2b69f4e560799e2bc9fe87d5e8)) +* Implement Tracking in .NET [#309](https://github.com/open-feature/dotnet-sdk/issues/309) ([#327](https://github.com/open-feature/dotnet-sdk/issues/327)) ([cbf4f25](https://github.com/open-feature/dotnet-sdk/commit/cbf4f25a4365eac15e37987d2d7163cb1aefacfe)) +* Support Returning Error Resolutions from Providers ([#323](https://github.com/open-feature/dotnet-sdk/issues/323)) ([bf9de4e](https://github.com/open-feature/dotnet-sdk/commit/bf9de4e177a4963340278854a25dd355f95dfc51)) + + +### 🧹 Chore + +* **deps:** update dependency fluentassertions to v7 ([#325](https://github.com/open-feature/dotnet-sdk/issues/325)) ([35cd77b](https://github.com/open-feature/dotnet-sdk/commit/35cd77b59dc938301e7e22ddefd9b39ef8e21a4b)) + ## [2.1.0](https://github.com/open-feature/dotnet-sdk/compare/v2.0.0...v2.1.0) (2024-11-18) diff --git a/README.md b/README.md index 1d87c74a..93ab04c1 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v2.1.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.1.0) + ![Release](https://img.shields.io/static/v1?label=release&message=v2.2.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.2.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index 21a7efd6..f17d78d5 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.1.0 + 2.2.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 7ec1d6db..ccbccc3d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.0 +2.2.0 From 583b2a9beab18ba70f8789b903d61a4c685560f0 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Mon, 16 Dec 2024 13:14:01 -0500 Subject: [PATCH 218/316] docs: disable space in link text lint rule (#329) A recent change in the markdown link rules made multi-line links a violation of MD039. https://github.com/DavidAnson/markdownlint/commit/a6cf08dfc654e5b7610cbb153d423d6a0f3b7907 This is impacting the doc updater. https://github.com/open-feature/openfeature.dev/actions/runs/12348450303/job/34457301528?pr=872 Signed-off-by: Michael Beemer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 93ab04c1..d9a277c2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + ![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) From fd68cb0bed0228607cc2369ef6822dd518c5fbec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 08:51:52 -0500 Subject: [PATCH 219/316] chore(deps): update actions/upload-artifact action to v4.5.0 (#332) 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 | |---|---|---|---| | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | minor | `v4.4.3` -> `v4.5.0` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.5.0`](https://redirect.github.com/actions/upload-artifact/compare/v4.4.3...v4.5.0) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.4.3...v4.5.0)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acd6d428..25f4ef37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.5.0 with: name: nupkgs path: src/**/*.nupkg From 6f5b04997aee44c2023e75471932e9f5ff27b0be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:53:46 -0500 Subject: [PATCH 220/316] chore(deps): update dependency microsoft.net.test.sdk to 17.12.0 (#322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Microsoft.NET.Test.Sdk](https://redirect.github.com/microsoft/vstest) | `17.11.1` -> `17.12.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.NET.Test.Sdk/17.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.NET.Test.Sdk/17.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.NET.Test.Sdk/17.11.1/17.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.NET.Test.Sdk/17.11.1/17.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
microsoft/vstest (Microsoft.NET.Test.Sdk) ### [`v17.12.0`](https://redirect.github.com/microsoft/vstest/releases/tag/v17.12.0) #### What's Changed - Dispose IDisposables in HtmlTransformer by [@​omajid](https://redirect.github.com/omajid) in [https://github.com/microsoft/vstest/pull/5099](https://redirect.github.com/microsoft/vstest/pull/5099) - Dipose XmlReaders in Microsoft.TestPlatform.Common.RunSettings by [@​omajid](https://redirect.github.com/omajid) in [https://github.com/microsoft/vstest/pull/5100](https://redirect.github.com/microsoft/vstest/pull/5100) - use some collection expressions by [@​SimonCropp](https://redirect.github.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5055](https://redirect.github.com/microsoft/vstest/pull/5055) - Fix Reference typos by [@​SimonCropp](https://redirect.github.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5155](https://redirect.github.com/microsoft/vstest/pull/5155) - Add option to overwrite trx without warning by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5141](https://redirect.github.com/microsoft/vstest/pull/5141) #### Internal and infrastructure fixes: - Downgrade xunit skip warning to info by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/10379](https://redirect.github.com/microsoft/vstest/pull/10379) - Fallback to latest runtimeconfig when none is found by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5136](https://redirect.github.com/microsoft/vstest/pull/5136) - Verify architecture and version of produced exes by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5134](https://redirect.github.com/microsoft/vstest/pull/5134) - Fix runtime config tests by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5137](https://redirect.github.com/microsoft/vstest/pull/5137) - Dispose helper when parsing args by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5126](https://redirect.github.com/microsoft/vstest/pull/5126) - Cleanup and bump required runtimes by [@​Evangelink](https://redirect.github.com/Evangelink) in [https://github.com/microsoft/vstest/pull/5139](https://redirect.github.com/microsoft/vstest/pull/5139) - Fix help warnings by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5140](https://redirect.github.com/microsoft/vstest/pull/5140) - Fix timing in simple log by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5143](https://redirect.github.com/microsoft/vstest/pull/5143) - Check vstest.console.dll instead of .exe by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5149](https://redirect.github.com/microsoft/vstest/pull/5149) - Report version from nuget check by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5161](https://redirect.github.com/microsoft/vstest/pull/5161) - Move IncludeSourceRevisionInInformationalVersion by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5166](https://redirect.github.com/microsoft/vstest/pull/5166) - Enable or disable new logger based on TL flag by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5167](https://redirect.github.com/microsoft/vstest/pull/5167) - Updating Microsoft.CodeCoverage package structure by [@​fhnaseer](https://redirect.github.com/fhnaseer) in [https://github.com/microsoft/vstest/pull/5169](https://redirect.github.com/microsoft/vstest/pull/5169) - Wait for Discovery to initialize before Cancelling it by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5177](https://redirect.github.com/microsoft/vstest/pull/5177) - Adding condition to disable MsCoverage refrenced path maps by [@​fhnaseer](https://redirect.github.com/fhnaseer) in [https://github.com/microsoft/vstest/pull/5189](https://redirect.github.com/microsoft/vstest/pull/5189) - Forward error output from testhost as info by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5192](https://redirect.github.com/microsoft/vstest/pull/5192) - Update Microsoft.Extensions.DependencyModel to 3.1.0 by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5188](https://redirect.github.com/microsoft/vstest/pull/5188) - ExcludeFromSourceBuild->ExcludeFromSourceOnlyBuild by [@​mmitche](https://redirect.github.com/mmitche) in [https://github.com/microsoft/vstest/pull/10354](https://redirect.github.com/microsoft/vstest/pull/10354) - Enable policheck by [@​jakubch1](https://redirect.github.com/jakubch1) in [https://github.com/microsoft/vstest/pull/10363](https://redirect.github.com/microsoft/vstest/pull/10363) **Full Changelog**: https://github.com/microsoft/vstest/compare/v17.11.1...v17.12.0
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c9995fc8..1dbc878a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + From 6c4cd0273f85bc0be0b07753d47bf13a613bbf82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:46:00 +0000 Subject: [PATCH 221/316] chore(deps): update codecov/codecov-action action to v5 (#316) --- .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 58e74f1f..6d53ef1b 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -36,7 +36,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v4.6.0 + - uses: codecov/codecov-action@v5.1.1 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From b9ebddfccb094f45a50e8196e43c087b4e97ffa4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:09:14 +0000 Subject: [PATCH 222/316] chore(deps): update codecov/codecov-action action to v5.1.2 (#334) --- .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 6d53ef1b..84742a10 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -36,7 +36,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v5.1.1 + - uses: codecov/codecov-action@v5.1.2 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From e14ab39180d38544132e9fe92244b7b37255d2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:33:58 +0000 Subject: [PATCH 223/316] fix: Adding Async Lifetime method to fix flaky unit tests (#333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR - Changes the way we currently reset the Api between unit tests. This approach should be safer since it calls the official `Shutdown`, and when we call the API again, all the resources are reset. ### Notes Check for more details: https://xunit.net/docs/shared-context#:~:text=For%20context%20cleanup%2C%20add%20the%20IDisposable%20interface%20to,call%20IAsyncDisposable%20%28it%20is%20planned%20for%20xUnit%20v3%29. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Api.cs | 10 +++++++++- .../ClearOpenFeatureInstanceFixture.cs | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index fae9916b..bc0499dd 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -29,7 +29,7 @@ public sealed class Api : IEventBus /// /// Singleton instance of Api /// - public static Api Instance { get; } = new Api(); + public static Api Instance { get; private set; } = new Api(); // Explicit static constructor to tell C# compiler // not to mark type as beforeFieldInit @@ -300,5 +300,13 @@ private async Task AfterError(FeatureProvider provider, Exception? ex) 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/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs index 5a620214..c3351801 100644 --- a/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs +++ b/test/OpenFeature.Tests/ClearOpenFeatureInstanceFixture.cs @@ -1,14 +1,20 @@ -using System; +using System.Threading.Tasks; +using Xunit; namespace OpenFeature.Tests; -public class ClearOpenFeatureInstanceFixture : IDisposable +public class ClearOpenFeatureInstanceFixture : IAsyncLifetime { + public Task InitializeAsync() + { + Api.ResetApi(); + + return Task.CompletedTask; + } + // Make sure the singleton is cleared between tests - public void Dispose() + public async Task DisposeAsync() { - Api.Instance.SetContext(null); - Api.Instance.ClearHooks(); - Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).Wait(); + await Api.Instance.ShutdownAsync().ConfigureAwait(false); } } From 2774b0d3c09f2f206834ca3fe2526e3eb3ca8087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:18:16 +0000 Subject: [PATCH 224/316] feat: add dotnet 9 support, rm dotnet 6 (#317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR - Updates all dotnet packages - Updates the Target Framework - Updates all GH actions - Removes net6.0 - Updates AutoFixture to a beta version ### Notes - .net6 is now EOF by Microsoft, so we should remove it. See https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core for reference - I needed to use a beta version of AutoFixture since, with .net9, it is more strict in package vulnerabilities, and a library version used by AutoFixture has vulnerabilities. See https://learn.microsoft.com/en-us/nuget/release-notes/nuget-6.12 for extra Nuget information and https://github.com/AutoFixture/AutoFixture/issues/1481 for AutoFixture --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Artyom Tonoyan --- .github/workflows/ci.yml | 4 +-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- .vscode/launch.json | 4 +-- Directory.Packages.props | 30 +++++++++---------- README.md | 3 +- global.json | 5 ++-- .../OpenFeature.DependencyInjection.csproj | 4 +-- .../OpenFeature.Hosting.csproj | 2 +- src/OpenFeature/OpenFeature.csproj | 2 +- .../OpenFeature.Benchmarks.csproj | 2 +- ...enFeature.DependencyInjection.Tests.csproj | 2 +- .../OpenFeature.E2ETests.csproj | 2 +- .../OpenFeature.IntegrationTests.csproj | 2 +- .../OpenFeature.Tests.csproj | 2 +- 17 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25f4ef37..f5f5152c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Restore @@ -66,8 +66,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Restore diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 84742a10..bbfd853a 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -29,8 +29,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Run Test diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 9af0ae8b..a6d5e36e 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -17,7 +17,7 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: dotnet format run: dotnet format --verify-no-changes OpenFeature.sln diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 914d6809..0be1db57 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -24,8 +24,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Initialize Tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b51c9bff..574aa6ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,8 +36,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install dependencies diff --git a/.vscode/launch.json b/.vscode/launch.json index 4d46e206..c530628b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/test/OpenFeature.Tests/bin/Debug/net6.0/OpenFeature.Tests.dll", + "program": "${workspaceFolder}/test/OpenFeature.Tests/bin/Debug/net9.0/OpenFeature.Tests.dll", "args": [], "cwd": "${workspaceFolder}/test/OpenFeature.Tests", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console @@ -23,4 +23,4 @@ "request": "attach" } ] -} \ No newline at end of file +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 1dbc878a..8c46f9f9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,23 +1,23 @@ - + true - + - - - - - - - - + + + + + + + + - + - + @@ -30,11 +30,11 @@ - + - + - + diff --git a/README.md b/README.md index d9a277c2..ad7d5223 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ ### Requirements -- .NET 6+ -- .NET Core 6+ +- .NET 8+ - .NET Framework 4.6.2+ Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 diff --git a/global.json b/global.json index 0ee5ec22..a35b4984 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { "sdk": { - "rollForward": "latestFeature", - "version": "8.0.404" + "rollForward": "latestMajor", + "version": "9.0.100", + "allowPrerelease": false } } diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 895c45f3..6f8163fb 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ -ο»Ώ + - netstandard2.0;net6.0;net8.0;net462 + netstandard2.0;net8.0;net9.0;net462 enable enable OpenFeature.DependencyInjection diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj index 48730084..43237e0f 100644 --- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 enable enable OpenFeature diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index ed991c4e..357d39c5 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net6.0;net8.0;net462 + net8.0;net9.0;netstandard2.0;net462 OpenFeature README.md diff --git a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj index 974dce5c..c0dc300a 100644 --- a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj +++ b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 OpenFeature.Benchmark Exe diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj index 9937e1bc..e4c16ee5 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 enable enable diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index d91b338e..50cf1a6a 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 $(TargetFrameworks);net462 OpenFeature.E2ETests diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj index 8287b2ec..13c2f21e 100644 --- a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj +++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index bfadbf9b..4df0c681 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 $(TargetFrameworks);net462 OpenFeature.Tests From dd26ad6d35e134ab40a290e644d5f8bdc8e56c66 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:24:30 +0000 Subject: [PATCH 225/316] chore(deps): update dependency dotnet-sdk to v9.0.101 (#339) --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index a35b4984..5ead15e7 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMajor", - "version": "9.0.100", + "version": "9.0.101", "allowPrerelease": false } } From 26fd2356c1835271dee2f7b8b03b2c83e9cb2eea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:31:49 +0000 Subject: [PATCH 226/316] chore(deps): update dependency coverlet.msbuild to 6.0.3 (#337) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8c46f9f9..122d7239 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + From 8527b03fb020a9604463da80f305978baa85f172 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:02:37 +0000 Subject: [PATCH 227/316] chore(deps): update dependency coverlet.collector to 6.0.3 (#336) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 122d7239..ab69e552 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From 2ef995529d377826d467fa486f18af20bfeeba60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:48:09 +0000 Subject: [PATCH 228/316] feat: Add evaluation details to finally hook stage (#335) 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> --- README.md | 2 +- src/OpenFeature/Hook.cs | 21 ++++-- src/OpenFeature/OpenFeatureClient.cs | 12 ++-- .../OpenFeatureClientTests.cs | 20 ++++++ .../OpenFeature.Tests/OpenFeatureHookTests.cs | 70 ++++++++++--------- test/OpenFeature.Tests/TestImplementations.cs | 1 + 6 files changed, 80 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index ad7d5223..4eefbd32 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ public class MyHook : Hook // code to run if there's an error during before hooks or during flag evaluation } - public ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null) + public ValueTask FinallyAsync(HookContext context, FlagEvaluationDetails evaluationDetails, IReadOnlyDictionary hints = null) { // code to run after all other stages, regardless of success/failure } diff --git a/src/OpenFeature/Hook.cs b/src/OpenFeature/Hook.cs index aea5dc15..c1dbbe38 100644 --- a/src/OpenFeature/Hook.cs +++ b/src/OpenFeature/Hook.cs @@ -31,7 +31,8 @@ public abstract class Hook /// Flag value type (bool|number|string|object) /// Modified EvaluationContext that is used for the flag evaluation public virtual ValueTask BeforeAsync(HookContext context, - IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { return new ValueTask(EvaluationContext.Empty); } @@ -44,8 +45,10 @@ public virtual ValueTask BeforeAsync(HookContext contex /// Caller provided data /// The . /// Flag value type (bool|number|string|object) - public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + public virtual ValueTask AfterAsync(HookContext context, + FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { return new ValueTask(); } @@ -58,8 +61,10 @@ public virtual ValueTask AfterAsync(HookContext context, FlagEvaluationDet /// Caller provided data /// The . /// Flag value type (bool|number|string|object) - public virtual ValueTask ErrorAsync(HookContext context, Exception error, - IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + public virtual ValueTask ErrorAsync(HookContext context, + Exception error, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { return new ValueTask(); } @@ -68,10 +73,14 @@ public virtual ValueTask ErrorAsync(HookContext context, Exception error, /// Called unconditionally after flag evaluation. /// /// Provides context of innovation + /// Flag evaluation information /// Caller provided data /// The . /// Flag value type (bool|number|string|object) - public virtual ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + public virtual ValueTask FinallyAsync(HookContext context, + FlagEvaluationDetails evaluationDetails, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { return new ValueTask(); } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index e774c6b5..787c89ee 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -244,7 +244,7 @@ private async Task> EvaluateFlagAsync( evaluationContextBuilder.Build() ); - FlagEvaluationDetails evaluation; + FlagEvaluationDetails? evaluation = null; try { var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false); @@ -297,7 +297,9 @@ await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, opti } finally { - await this.TriggerFinallyHooksAsync(allHooksReversed, hookContext, options, cancellationToken).ConfigureAwait(false); + evaluation ??= new FlagEvaluationDetails(flagKey, defaultValue, ErrorType.General, Reason.Error, string.Empty, + "Evaluation failed to return a result."); + await this.TriggerFinallyHooksAsync(allHooksReversed, evaluation, hookContext, options, cancellationToken).ConfigureAwait(false); } return evaluation; @@ -351,14 +353,14 @@ private async Task TriggerErrorHooksAsync(IReadOnlyList hooks, HookCont } } - private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, HookContext context, - FlagEvaluationOptions? options, CancellationToken cancellationToken = default) + private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, FlagEvaluationDetails evaluation, + HookContext context, FlagEvaluationOptions? options, CancellationToken cancellationToken = default) { foreach (var hook in hooks) { try { - await hook.FinallyAsync(context, options?.HookHints, cancellationToken).ConfigureAwait(false); + await hook.FinallyAsync(context, evaluation, options?.HookHints, cancellationToken).ConfigureAwait(false); } catch (Exception e) { diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 13d3fa93..1cab2d76 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -656,5 +656,25 @@ public async Task TheClient_MergesTheEvaluationContextInTheCorrectOrder(string k Assert.Equal(expectedResult, actualEvaluationContext.GetValue(key).AsString); } + + [Fact] + [Specification("4.3.8", "'evaluation details' passed to the 'finally' stage matches the evaluation details returned to the application author")] + public async Task FinallyHook_IncludesEvaluationDetails() + { + // Arrange + var provider = new TestProvider(); + var providerHook = Substitute.For(); + provider.AddHook(providerHook); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); + + const string flagName = "flagName"; + + // Act + var evaluationDetails = await client.GetBooleanDetailsAsync(flagName, true); + + // Assert + await providerHook.Received(1).FinallyAsync(Arg.Any>(), evaluationDetails); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index cc8b08a1..48de5ee5 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -45,10 +45,10 @@ public async Task Hooks_Should_Be_Called_In_Order() invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - providerHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - clientHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - apiHook.FinallyAsync(Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); var testProvider = new TestProvider(); testProvider.AddHook(providerHook); @@ -70,10 +70,10 @@ await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empt invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - providerHook.FinallyAsync(Arg.Any>(), Arg.Any>()); - invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>()); - clientHook.FinallyAsync(Arg.Any>(), Arg.Any>()); - apiHook.FinallyAsync(Arg.Any>(), Arg.Any>()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); }); _ = apiHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); @@ -84,10 +84,10 @@ await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empt _ = invocationHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); _ = clientHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); _ = apiHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); - _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); } [Fact] @@ -239,10 +239,12 @@ public async Task Hook_Should_Return_No_Errors() }; var hookContext = new HookContext("test", false, FlagValueType.Boolean, new ClientMetadata(null, null), new Metadata(null), EvaluationContext.Empty); + var evaluationDetails = + new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"); await hook.BeforeAsync(hookContext, hookHints); - await hook.AfterAsync(hookContext, new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"), hookHints); - await hook.FinallyAsync(hookContext, hookHints); + await hook.AfterAsync(hookContext, evaluationDetails, hookHints); + await hook.FinallyAsync(hookContext, evaluationDetails, hookHints); await hook.ErrorAsync(hookContext, new Exception(), hookHints); hookContext.ClientMetadata.Name.Should().BeNull(); @@ -269,7 +271,7 @@ public async Task Hook_Should_Execute_In_Correct_Order() hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); @@ -282,12 +284,12 @@ public async Task Hook_Should_Execute_In_Correct_Order() hook.BeforeAsync(Arg.Any>(), Arg.Any>()); featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.FinallyAsync(Arg.Any>(), Arg.Any>()); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); }); _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); } @@ -331,8 +333,8 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); - hook2.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask()); - hook1.FinallyAsync(Arg.Any>(), null).Throws(new Exception()); + hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Throws(new Exception()); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); @@ -348,8 +350,8 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null); hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null); - hook2.FinallyAsync(Arg.Any>(), null); - hook1.FinallyAsync(Arg.Any>(), null); + hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null); + hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null); }); _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); @@ -357,8 +359,8 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); _ = hook2.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); _ = hook1.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); - _ = hook2.Received(1).FinallyAsync(Arg.Any>(), null); - _ = hook1.Received(1).FinallyAsync(Arg.Any>(), null); + _ = hook2.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook1.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); } [Fact] @@ -458,7 +460,7 @@ public async Task Hook_Hints_May_Be_Optional() hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) .Returns(new ValueTask()); - hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) .Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider); @@ -471,7 +473,7 @@ public async Task Hook_Hints_May_Be_Optional() hook.Received().BeforeAsync(Arg.Any>(), Arg.Any>()); featureProvider.Received().ResolveBooleanValueAsync("test", false, Arg.Any()); hook.Received().AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>()); + hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); }); } @@ -489,7 +491,7 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() // Sequence hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(exceptionToThrow); hook.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); - hook.FinallyAsync(Arg.Any>(), null).Returns(new ValueTask()); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); var client = Api.Instance.GetClient(); client.AddHooks(hook); @@ -500,13 +502,13 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() { hook.BeforeAsync(Arg.Any>(), Arg.Any>()); hook.ErrorAsync(Arg.Any>(), Arg.Any(), null); - hook.FinallyAsync(Arg.Any>(), null); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null); }); resolvedFlag.Should().BeTrue(); _ = hook.Received(1).BeforeAsync(Arg.Any>(), null); _ = hook.Received(1).ErrorAsync(Arg.Any>(), exceptionToThrow, null); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), null); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); } [Fact] @@ -536,7 +538,7 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) .Returns(new ValueTask()); - hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) .Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider); @@ -550,7 +552,7 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() { hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>()); + hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); }); await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); @@ -569,7 +571,7 @@ public async Task Successful_Resolution_Should_Pass_Cancellation_Token() hook.BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token).Returns(EvaluationContext.Empty); featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any(), cts.Token).Returns(new ResolutionDetails("test", false)); _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); - _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); @@ -579,7 +581,7 @@ public async Task Successful_Resolution_Should_Pass_Cancellation_Token() _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); } [Fact] @@ -606,7 +608,7 @@ public async Task Failed_Resolution_Should_Pass_Cancellation_Token() hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) .Returns(new ValueTask()); - hook.FinallyAsync(Arg.Any>(), Arg.Any>()) + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) .Returns(new ValueTask()); await Api.Instance.SetProviderAsync(featureProvider); @@ -616,7 +618,7 @@ public async Task Failed_Resolution_Should_Pass_Cancellation_Token() _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); _ = hook.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>(), cts.Token); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index aa4dc784..724278e8 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -48,6 +48,7 @@ public override ValueTask ErrorAsync(HookContext context, Exception error, } public override ValueTask FinallyAsync(HookContext context, + FlagEvaluationDetails evaluationDetails, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { Interlocked.Increment(ref this._finallyCallCount); From 1b5a0a9823e4f68e9356536ad5aa8418d8ca815f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:40:10 +0000 Subject: [PATCH 229/316] feat: Implement transaction context (#312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Transaction Context This pull request introduces transaction context propagation to the OpenFeature library. The changes include adding a new interface for transaction context propagation, implementing a no-op and an AsyncLocal-based propagator, and updating the API to support these propagators. ### Related Issues Fixes #243 ### Notes #### Transaction Context Propagation: * [`src/OpenFeature/Api.cs`](diffhunk://#diff-dc150fbd3b7be797470374ee7e38c7ab8a31eb9b6b7a52345e05114c2d32a15eR25-R26): Added a private field `_transactionContextPropagator` and a lock for thread safety. Introduced methods to get and set the transaction context propagator, and to manage the transaction context using the propagator. [[1]](diffhunk://#diff-dc150fbd3b7be797470374ee7e38c7ab8a31eb9b6b7a52345e05114c2d32a15eR25-R26) [[2]](diffhunk://#diff-dc150fbd3b7be797470374ee7e38c7ab8a31eb9b6b7a52345e05114c2d32a15eR222-R274) * [`src/OpenFeature/AsyncLocalTransactionContextPropagator.cs`](diffhunk://#diff-d9ca58e32d696079f875c51837dfc6ded087b06eb3aef3513f5ea15ebc22c700R1-R25): Implemented the `AsyncLocalTransactionContextPropagator` class that uses `AsyncLocal` to store the transaction context. * [`src/OpenFeature/NoOpTransactionContextPropagator.cs`](diffhunk://#diff-09ab422ed267155042b791de4d1c88f1bd82cb68d5f541a92c6af4318ceacd6aR1-R15): Implemented a no-op version of the `ITransactionContextPropagator` interface. * [`src/OpenFeature/Model/ITransactionContextPropagator.cs`](diffhunk://#diff-614f5b3e42871f4057f04d5fd27bf56157315e1c822ff0c83403255a8bf163ecR1-R26): Defined the `ITransactionContextPropagator` interface responsible for persisting transaction contexts. #### API Enhancements: * [`src/OpenFeature/Api.cs`](diffhunk://#diff-dc150fbd3b7be797470374ee7e38c7ab8a31eb9b6b7a52345e05114c2d32a15eR291): Updated the `ShutdownAsync` method to reset the transaction context propagator. * [`src/OpenFeature/OpenFeatureClient.cs`](diffhunk://#diff-c23c8a3ea4538fbdcf6b1cf93ea3de456906e4d267fc4b2ba3f8b1cb186a7907R224): Modified the `EvaluateFlagAsync` method to merge the transaction context with the evaluation context. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Todd Baert Co-authored-by: Todd Baert --- README.md | 47 +++++++++--- src/OpenFeature/Api.cs | 59 ++++++++++++++- .../AsyncLocalTransactionContextPropagator.cs | 25 +++++++ .../Model/ITransactionContextPropagator.cs | 26 +++++++ .../NoOpTransactionContextPropagator.cs | 15 ++++ src/OpenFeature/OpenFeatureClient.cs | 10 +-- ...cLocalTransactionContextPropagatorTests.cs | 58 +++++++++++++++ .../OpenFeature.Tests/OpenFeatureHookTests.cs | 17 +++++ test/OpenFeature.Tests/OpenFeatureTests.cs | 73 +++++++++++++++++++ 9 files changed, 311 insertions(+), 19 deletions(-) create mode 100644 src/OpenFeature/AsyncLocalTransactionContextPropagator.cs create mode 100644 src/OpenFeature/Model/ITransactionContextPropagator.cs create mode 100644 src/OpenFeature/NoOpTransactionContextPropagator.cs create mode 100644 test/OpenFeature.Tests/AsyncLocalTransactionContextPropagatorTests.cs diff --git a/README.md b/README.md index 4eefbd32..0aaeb399 100644 --- a/README.md +++ b/README.md @@ -68,18 +68,19 @@ public async Task Example() ## 🌟 Features -| Status | Features | Description | -| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| βœ… | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | -| βœ… | [Logging](#logging) | Integrate with popular logging packages. | -| βœ… | [Domains](#domains) | Logically bind clients with providers. | -| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | -| πŸ”¬ | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. | +| Status | Features | Description | +| ------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | +| βœ… | [Logging](#logging) | Integrate with popular logging packages. | +| βœ… | [Domains](#domains) | Logically bind clients with providers. | +| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). | +| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| πŸ”¬ | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. | > Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ | Experimental: πŸ”¬ @@ -234,6 +235,28 @@ The OpenFeature API provides a close function to perform a cleanup of all regist await Api.Instance.ShutdownAsync(); ``` +### Transaction Context Propagation + +Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP). +Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the transaction context propagator it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread). +By default, the `NoOpTransactionContextPropagator` is used, which doesn't store anything. +To register a [AsyncLocal](https://learn.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1) context propagator, you can use the `SetTransactionContextPropagator` method as shown below. + +```csharp +// registering the AsyncLocalTransactionContextPropagator +Api.Instance.SetTransactionContextPropagator(new AsyncLocalTransactionContextPropagator()); +``` +Once you've registered a transaction context propagator, you can propagate the data into request-scoped transaction context. + +```csharp +// adding userId to transaction context +EvaluationContext transactionContext = EvaluationContext.Builder() + .Set("userId", userId) + .Build(); +Api.Instance.SetTransactionContext(transactionContext); +``` +Additionally, you can develop a custom transaction context propagator by implementing the `TransactionContextPropagator` interface and registering it as shown above. + ## Extending ### Develop a provider diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index bc0499dd..70321883 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -22,6 +22,8 @@ public sealed class Api : IEventBus 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(); @@ -47,6 +49,7 @@ public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); + } /// @@ -85,7 +88,6 @@ public FeatureProvider 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) @@ -109,7 +111,6 @@ public FeatureProvider GetProvider(string domain) /// 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(); @@ -218,6 +219,59 @@ public EvaluationContext GetContext() } } + /// + /// 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) + { + throw new ArgumentNullException(nameof(transactionContextPropagator), + "Transaction context propagator cannot be null"); + } + + lock (this._transactionContextPropagatorLock) + { + 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(); + } + + /// + /// 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) + { + throw new ArgumentNullException(nameof(evaluationContext), "Evaluation context cannot be null"); + } + + this._transactionContextPropagator.SetTransactionContext(evaluationContext); + } + /// /// /// Shut down and reset the current status of OpenFeature API. @@ -234,6 +288,7 @@ public async Task ShutdownAsync() { 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(); diff --git a/src/OpenFeature/AsyncLocalTransactionContextPropagator.cs b/src/OpenFeature/AsyncLocalTransactionContextPropagator.cs new file mode 100644 index 00000000..86992aac --- /dev/null +++ b/src/OpenFeature/AsyncLocalTransactionContextPropagator.cs @@ -0,0 +1,25 @@ +using System.Threading; +using OpenFeature.Model; + +namespace OpenFeature; + +/// +/// This is a task transaction context implementation of +/// It uses the to store the transaction context. +/// +public sealed class AsyncLocalTransactionContextPropagator : ITransactionContextPropagator +{ + private readonly AsyncLocal _transactionContext = new(); + + /// + public EvaluationContext GetTransactionContext() + { + return this._transactionContext.Value ?? EvaluationContext.Empty; + } + + /// + public void SetTransactionContext(EvaluationContext evaluationContext) + { + this._transactionContext.Value = evaluationContext; + } +} diff --git a/src/OpenFeature/Model/ITransactionContextPropagator.cs b/src/OpenFeature/Model/ITransactionContextPropagator.cs new file mode 100644 index 00000000..3fbc43c9 --- /dev/null +++ b/src/OpenFeature/Model/ITransactionContextPropagator.cs @@ -0,0 +1,26 @@ +namespace OpenFeature.Model; + +/// +/// is responsible for persisting a transactional context +/// for the duration of a single transaction. +/// Examples of potential transaction specific context include: a user id, user agent, IP. +/// Transaction context is merged with evaluation context prior to flag evaluation. +/// +/// +/// The precedence of merging context can be seen in +/// the specification. +/// +public interface ITransactionContextPropagator +{ + /// + /// Returns the currently defined transaction context using the registered transaction context propagator. + /// + /// The current transaction context + EvaluationContext GetTransactionContext(); + + /// + /// Sets the transaction context. + /// + /// The transaction context to be set + void SetTransactionContext(EvaluationContext evaluationContext); +} diff --git a/src/OpenFeature/NoOpTransactionContextPropagator.cs b/src/OpenFeature/NoOpTransactionContextPropagator.cs new file mode 100644 index 00000000..70f57cdc --- /dev/null +++ b/src/OpenFeature/NoOpTransactionContextPropagator.cs @@ -0,0 +1,15 @@ +using OpenFeature.Model; + +namespace OpenFeature; + +internal class NoOpTransactionContextPropagator : ITransactionContextPropagator +{ + public EvaluationContext GetTransactionContext() + { + return EvaluationContext.Empty; + } + + public void SetTransactionContext(EvaluationContext evaluationContext) + { + } +} diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 787c89ee..8c39621b 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -215,12 +215,12 @@ private async Task> EvaluateFlagAsync( // New up an evaluation context if one was not provided. context ??= EvaluationContext.Empty; - // merge api, client, and invocation context. - var evaluationContext = Api.Instance.GetContext(); + // merge api, client, transaction and invocation context var evaluationContextBuilder = EvaluationContext.Builder(); - evaluationContextBuilder.Merge(evaluationContext); - evaluationContextBuilder.Merge(this.GetContext()); - evaluationContextBuilder.Merge(context); + evaluationContextBuilder.Merge(Api.Instance.GetContext()); // API context + evaluationContextBuilder.Merge(this.GetContext()); // Client context + evaluationContextBuilder.Merge(Api.Instance.GetTransactionContext()); // Transaction context + evaluationContextBuilder.Merge(context); // Invocation context var allHooks = new List() .Concat(Api.Instance.GetHooks()) diff --git a/test/OpenFeature.Tests/AsyncLocalTransactionContextPropagatorTests.cs b/test/OpenFeature.Tests/AsyncLocalTransactionContextPropagatorTests.cs new file mode 100644 index 00000000..51c94bfb --- /dev/null +++ b/test/OpenFeature.Tests/AsyncLocalTransactionContextPropagatorTests.cs @@ -0,0 +1,58 @@ +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Tests; + +public class AsyncLocalTransactionContextPropagatorTests +{ + [Fact] + public void GetTransactionContext_ReturnsEmpty_WhenNoContextIsSet() + { + // Arrange + var propagator = new AsyncLocalTransactionContextPropagator(); + + // Act + var context = propagator.GetTransactionContext(); + + // Assert + Assert.Equal(EvaluationContext.Empty, context); + } + + [Fact] + public void SetTransactionContext_SetsAndGetsContextCorrectly() + { + // Arrange + var propagator = new AsyncLocalTransactionContextPropagator(); + var evaluationContext = EvaluationContext.Builder() + .Set("initial", "yes") + .Build(); + + // Act + propagator.SetTransactionContext(evaluationContext); + var context = propagator.GetTransactionContext(); + + // Assert + Assert.Equal(evaluationContext, context); + Assert.Equal(evaluationContext.GetValue("initial"), context.GetValue("initial")); + } + + [Fact] + public void SetTransactionContext_OverridesPreviousContext() + { + // Arrange + var propagator = new AsyncLocalTransactionContextPropagator(); + + var initialContext = EvaluationContext.Builder() + .Set("initial", "yes") + .Build(); + var newContext = EvaluationContext.Empty; + + // Act + propagator.SetTransactionContext(initialContext); + propagator.SetTransactionContext(newContext); + var context = propagator.GetTransactionContext(); + + // Assert + Assert.Equal(newContext, context); + } +} diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 48de5ee5..442b3491 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -166,6 +166,9 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() var propInvocation = "4.3.4invocation"; var propInvocationToOverwrite = "4.3.4invocationToOverwrite"; + var propTransaction = "4.3.4transaction"; + var propTransactionToOverwrite = "4.3.4transactionToOverwrite"; + var propHook = "4.3.4hook"; // setup a cascade of overwriting properties @@ -180,17 +183,29 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() .Set(propClientToOverwrite, false) .Build(); + var transactionContext = new EvaluationContextBuilder() + .Set(propTransaction, true) + .Set(propInvocationToOverwrite, true) + .Set(propTransactionToOverwrite, false) + .Build(); + var invocationContext = new EvaluationContextBuilder() .Set(propInvocation, true) .Set(propClientToOverwrite, true) + .Set(propTransactionToOverwrite, true) .Set(propInvocationToOverwrite, false) .Build(); + var hookContext = new EvaluationContextBuilder() .Set(propHook, true) .Set(propInvocationToOverwrite, true) .Build(); + var transactionContextPropagator = new AsyncLocalTransactionContextPropagator(); + transactionContextPropagator.SetTransactionContext(transactionContext); + Api.Instance.SetTransactionContextPropagator(transactionContextPropagator); + var provider = Substitute.For(); provider.GetMetadata().Returns(new Metadata(null)); @@ -212,7 +227,9 @@ public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() _ = provider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Is(y => (y.GetValue(propGlobal).AsBoolean ?? false) && (y.GetValue(propClient).AsBoolean ?? false) + && (y.GetValue(propTransaction).AsBoolean ?? false) && (y.GetValue(propGlobalToOverwrite).AsBoolean ?? false) + && (y.GetValue(propTransactionToOverwrite).AsBoolean ?? false) && (y.GetValue(propInvocation).AsBoolean ?? false) && (y.GetValue(propClientToOverwrite).AsBoolean ?? false) && (y.GetValue(propHook).AsBoolean ?? false) diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index acc53b61..1955f82d 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -244,5 +245,77 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() (await client1.GetBooleanValueAsync("test", false)).Should().BeTrue(); (await client2.GetBooleanValueAsync("test", false)).Should().BeFalse(); } + + [Fact] + public void SetTransactionContextPropagator_ShouldThrowArgumentNullException_WhenNullPropagatorIsPassed() + { + // Arrange + var api = Api.Instance; + + // Act & Assert + Assert.Throws(() => api.SetTransactionContextPropagator(null!)); + } + + [Fact] + public void SetTransactionContextPropagator_ShouldSetPropagator_WhenValidPropagatorIsPassed() + { + // Arrange + var api = Api.Instance; + var mockPropagator = Substitute.For(); + + // Act + api.SetTransactionContextPropagator(mockPropagator); + + // Assert + Assert.Equal(mockPropagator, api.GetTransactionContextPropagator()); + } + + [Fact] + public void SetTransactionContext_ShouldThrowArgumentNullException_WhenEvaluationContextIsNull() + { + // Arrange + var api = Api.Instance; + + // Act & Assert + Assert.Throws(() => api.SetTransactionContext(null!)); + } + + [Fact] + public void SetTransactionContext_ShouldSetTransactionContext_WhenValidEvaluationContextIsProvided() + { + // Arrange + var api = Api.Instance; + var evaluationContext = EvaluationContext.Builder() + .Set("initial", "yes") + .Build(); + var mockPropagator = Substitute.For(); + mockPropagator.GetTransactionContext().Returns(evaluationContext); + api.SetTransactionContextPropagator(mockPropagator); + api.SetTransactionContext(evaluationContext); + + // Act + api.SetTransactionContext(evaluationContext); + var result = api.GetTransactionContext(); + + // Assert + mockPropagator.Received().SetTransactionContext(evaluationContext); + Assert.Equal(evaluationContext, result); + Assert.Equal(evaluationContext.GetValue("initial"), result.GetValue("initial")); + } + + [Fact] + public void GetTransactionContext_ShouldReturnEmptyEvaluationContext_WhenNoPropagatorIsSet() + { + // Arrange + var api = Api.Instance; + var context = EvaluationContext.Builder().Set("status", "not-ready").Build(); + api.SetTransactionContext(context); + + // Act + var result = api.GetTransactionContext(); + + // Assert + Assert.Equal(EvaluationContext.Empty, result); + } } } From f994234ef53acfe557c00a9d3713bee1c7127964 Mon Sep 17 00:00:00 2001 From: Joseph Sawyer Date: Wed, 15 Jan 2025 16:55:51 +0000 Subject: [PATCH 230/316] ci: dotnet pack release build (#344) Signed-off-by: Joseph Sawyer --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/release.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5f5152c..cd3e4d23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,10 @@ jobs: run: dotnet restore - name: Build - run: dotnet build --no-restore + run: dotnet build -c Release --no-restore - name: Test - run: dotnet test --no-build --logger GitHubActions + run: dotnet test -c Release --no-build --logger GitHubActions packaging: needs: build @@ -75,11 +75,11 @@ jobs: - name: Pack NuGet packages (CI versions) if: startsWith(github.ref, 'refs/heads/') - run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + run: dotnet pack -c Release --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" - name: Pack NuGet packages (PR versions) if: startsWith(github.ref, 'refs/pull/') - run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + run: dotnet pack -c Release --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" - name: Publish NuGet packages (base) if: github.event.pull_request.head.repo.fork == false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 574aa6ea..050024d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: run: dotnet restore - name: Pack - run: dotnet pack --no-restore + run: dotnet pack -c Release --no-restore - name: Publish to Nuget run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.NUGET_TOKEN }}" --source https://api.nuget.org/v3/index.json From 728ae471625ab1ff5f166b60a5830afbaf9ad276 Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:14:36 +0000 Subject: [PATCH 231/316] fix: Fix issue with DI documentation (#350) ## This PR - Addresses issue with the README documentation for Dependency Injection Providers. ### Related Issues Fixes #349 ### Notes ### Follow-up Tasks ### How to test --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> Co-authored-by: Michael Beemer --- README.md | 51 ++++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 0aaeb399..31a47041 100644 --- a/README.md +++ b/README.md @@ -379,19 +379,21 @@ builder.Services.AddOpenFeature(featureBuilder => { You can register a custom provider, such as `InMemoryProvider`, with OpenFeature using the `AddProvider` method. This approach allows you to dynamically resolve services or configurations during registration. ```csharp -services.AddOpenFeature() - .AddProvider(provider => +services.AddOpenFeature(builder => +{ + builder.AddProvider(provider => + { + // Resolve services or configurations as needed + var variants = new Dictionary { { "on", true } }; + var flags = new Dictionary { - // Resolve services or configurations as needed - var configuration = provider.GetRequiredService(); - var flags = new Dictionary - { - { "feature-key", new Flag(configuration.GetValue("FeatureFlags:Key")) } - }; - - // Register a custom provider, such as InMemoryProvider - return new InMemoryProvider(flags); - }); + { "feature-key", new Flag(variants, "on") } + }; + + // Register a custom provider, such as InMemoryProvider + return new InMemoryProvider(flags); + }); +}); ``` #### Adding a Domain-Scoped Provider @@ -399,18 +401,21 @@ services.AddOpenFeature() You can also register a domain-scoped custom provider, enabling configurations specific to each domain: ```csharp -services.AddOpenFeature() - .AddProvider("my-domain", (provider, domain) => +services.AddOpenFeature(builder => +{ + builder.AddProvider("my-domain", (provider, domain) => + { + // Resolve services or configurations as needed for the domain + var variants = new Dictionary { { "on", true } }; + var flags = new Dictionary { - // Resolve services or configurations as needed for the domain - var flags = new Dictionary - { - { $"{domain}-feature-key", new Flag(true) } - }; - - // Register a domain-scoped custom provider such as InMemoryProvider - return new InMemoryProvider(flags); - }); + { $"{domain}-feature-key", new Flag(variants, "on") } + }; + + // Register a domain-scoped custom provider such as InMemoryProvider + return new InMemoryProvider(flags); + }); +}); ``` From 7013e9503f6721bd5f241c6c4d082a4a4e9eceed Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:19:27 +0000 Subject: [PATCH 232/316] feat: Implement Default Logging Hook (#308) Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --- Directory.Packages.props | 1 + README.md | 13 + src/OpenFeature/Hooks/LoggingHook.cs | 174 +++++ .../Hooks/LoggingHookTests.cs | 674 ++++++++++++++++++ .../OpenFeature.Tests.csproj | 3 +- 5 files changed, 864 insertions(+), 1 deletion(-) create mode 100644 src/OpenFeature/Hooks/LoggingHook.cs create mode 100644 test/OpenFeature.Tests/Hooks/LoggingHookTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ab69e552..e83d449a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,6 +23,7 @@ + diff --git a/README.md b/README.md index 31a47041..857eae25 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,19 @@ var value = await client.GetBooleanValueAsync("boolFlag", false, context, new Fl The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. +#### Logging Hook + +The .NET SDK includes a LoggingHook, which logs detailed information at key points during flag evaluation, using Microsoft.Extensions.Logging structured logging API. This hook can be particularly helpful for troubleshooting and debugging; simply attach it at the global, client or invocation level and ensure your log level is set to "debug". + +```csharp +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("Program"); + +var client = Api.Instance.GetClient(); +client.AddHooks(new LoggingHook(logger)); +``` +See [hooks](#hooks) for more information on configuring hooks. + ### Domains Clients can be assigned to a domain. diff --git a/src/OpenFeature/Hooks/LoggingHook.cs b/src/OpenFeature/Hooks/LoggingHook.cs new file mode 100644 index 00000000..a8d318b0 --- /dev/null +++ b/src/OpenFeature/Hooks/LoggingHook.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OpenFeature.Model; + +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; + + /// + /// 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; + } + + /// + 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); + + this.HookBeforeStageExecuted(content); + + 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; + + var content = new LoggingHookContent( + context.ClientMetadata.Name, + context.ProviderMetadata.Name, + context.FlagKey, + context.DefaultValue?.ToString(), + evaluationContext); + + this.HookErrorStageExecuted(content); + + 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; + + var content = new LoggingHookContent( + context.ClientMetadata.Name, + context.ProviderMetadata.Name, + context.FlagKey, + context.DefaultValue?.ToString(), + evaluationContext); + + this.HookAfterStageExecuted(content); + + 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; + + 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; + } + + public override string ToString() + { + var stringBuilder = new StringBuilder(); + + stringBuilder.Append("Domain:"); + stringBuilder.AppendLine(this._domain); + + stringBuilder.Append("ProviderName:"); + stringBuilder.AppendLine(this._providerName); + + stringBuilder.Append("FlagKey:"); + stringBuilder.AppendLine(this._flagKey); + + stringBuilder.Append("DefaultValue:"); + stringBuilder.AppendLine(this._defaultValue); + + 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)); + } + } + + return stringBuilder.ToString(); + } + + static string? GetValueString(Value value) + { + if (value.IsNull) + return string.Empty; + + if (value.IsString) + return value.AsString; + + if (value.IsBoolean) + return value.AsBoolean.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/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs new file mode 100644 index 00000000..51ec8cb1 --- /dev/null +++ b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs @@ -0,0 +1,674 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using OpenFeature.Constant; +using OpenFeature.Hooks; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Tests.Hooks +{ + [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] + + public class LoggingHookTests + { + [Fact] + public async Task BeforeAsync_Without_EvaluationContext_Generates_Debug_Log() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + // Act + await hook.BeforeAsync(context); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + + Assert.Equal(LogLevel.Debug, record.Level); + Assert.Equal( + """ + Before Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + + """, + record.Message); + } + + [Fact] + public async Task BeforeAsync_Without_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + // Act + await hook.BeforeAsync(context); + + // Assert + var record = logger.LatestRecord; + + Assert.Equal( + """ + Before Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + + """, + record.Message); + } + + [Fact] + public async Task BeforeAsync_With_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", "value") + .Set("key_2", false) + .Set("key_3", 1.531) + .Set("key_4", 42) + .Set("key_5", DateTime.Parse("2025-01-01T11:00:00.0000000Z")) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.BeforeAsync(context); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + + Assert.Contains( + """ + Before Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + """, + record.Message); + Assert.Multiple( + () => Assert.Contains("key_1:value", record.Message), + () => Assert.Contains("key_2:False", record.Message), + () => Assert.Contains("key_3:1.531", record.Message), + () => Assert.Contains("key_4:42", record.Message), + () => Assert.Contains("key_5:2025-01-01T11:00:00.0000000+00:00", record.Message) + ); + } + + [Fact] + public async Task BeforeAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + // Act + var hook = new LoggingHook(logger, includeContext: true); + + await hook.BeforeAsync(context); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + + Assert.Equal( + """ + Before Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + + """, + record.Message); + } + + [Fact] + public async Task ErrorAsync_Without_EvaluationContext_Generates_Error_Log() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + var exception = new Exception("Error within hook!"); + + // Act + await hook.ErrorAsync(context, exception); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + + Assert.Equal(LogLevel.Error, record.Level); + } + + [Fact] + public async Task ErrorAsync_Without_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + var exception = new Exception("Error within hook!"); + + // Act + await hook.ErrorAsync(context, exception); + + // Assert + var record = logger.LatestRecord; + + Assert.Equal( + """ + Error during Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + + """, + record.Message); + } + + [Fact] + public async Task ErrorAsync_With_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", " ") + .Set("key_2", true) + .Set("key_3", 0.002154) + .Set("key_4", -15) + .Set("key_5", DateTime.Parse("2099-01-01T01:00:00.0000000Z")) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var hook = new LoggingHook(logger, includeContext: true); + + var exception = new Exception("Error within hook!"); + + // Act + await hook.ErrorAsync(context, exception); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Error, record.Level); + + Assert.Contains( + """ + Error during Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + """, + record.Message); + Assert.Multiple( + () => Assert.Contains("key_1: ", record.Message), + () => Assert.Contains("key_2:True", record.Message), + () => Assert.Contains("key_3:0.002154", record.Message), + () => Assert.Contains("key_4:-15", record.Message), + () => Assert.Contains("key_5:2099-01-01T01:00:00.0000000+00:00", record.Message) + ); + } + + [Fact] + public async Task ErrorAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: true); + + var exception = new Exception("Error within hook!"); + + // Act + await hook.ErrorAsync(context, exception); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Error, record.Level); + + Assert.Equal( + """ + Error during Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + + """, + record.Message); + } + + [Fact] + public async Task AfterAsync_Without_EvaluationContext_Generates_Debug_Log() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: false); + + // Act + await hook.AfterAsync(context, details); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + } + + [Fact] + public async Task AfterAsync_Without_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: false); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + + """, + record.Message); + } + + [Fact] + public async Task AfterAsync_With_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", "") + .Set("key_2", false) + .Set("key_3", double.MinValue) + .Set("key_4", int.MaxValue) + .Set("key_5", DateTime.MinValue) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + + Assert.Contains( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + """, + record.Message); + + // .NET Framework uses G15 formatter on double.ToString + // .NET uses G17 formatter on double.ToString +#if NET462 + var expectedMaxDoubleString = "-1.79769313486232E+308"; +#else + var expectedMaxDoubleString = "-1.7976931348623157E+308"; +#endif + Assert.Multiple( + () => Assert.Contains("key_1:", record.Message), + () => Assert.Contains("key_2:False", record.Message), + () => Assert.Contains($"key_3:{expectedMaxDoubleString}", record.Message), + () => Assert.Contains("key_4:2147483647", record.Message), + () => Assert.Contains("key_5:0001-01-01T00:00:00.0000000", record.Message) + ); + } + + [Fact] + public async Task AfterAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + + """, + record.Message); + } + + [Fact] + public void Create_LoggingHook_Without_Logger() + { + Assert.Throws(() => new LoggingHook(null!, includeContext: true)); + } + + [Fact] + public async Task With_Structure_Type_In_Context_Returns_Qualified_Name_Of_Value() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", Structure.Builder().Set("inner_key_1", false).Build()) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + + // Raw string literals will convert tab to spaces (the File index style) + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + key_1:OpenFeature.Model.Value + + """, + message + ); + } + + [Fact] + public async Task Without_Domain_Returns_Missing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata(null, "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", true) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:missing + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + key_1:True + + """, + message + ); + } + + [Fact] + public async Task Without_Provider_Returns_Missing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata(null); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", true) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:missing + FlagKey:test + DefaultValue:False + Context: + key_1:True + + """, + message + ); + } + + [Fact] + public async Task Without_DefaultValue_Returns_Missing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", true) + .Build(); + + var context = new HookContext("test", null!, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", "true", ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:missing + Context: + key_1:True + + """, + message + ); + } + + [Fact] + public async Task Without_EvaluationContextValue_Returns_Nothing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", (string)null!) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + key_1: + + """, + message + ); + } + + private static string NormalizeLogRecord(FakeLogRecord record) + { + // Raw string literals will convert tab to spaces (the File index style) + const int tabSize = 4; + + return record.Message.Replace("\t", new string(' ', tabSize)); + } + } +} diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 4df0c681..270b6a50 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -1,4 +1,4 @@ - +ο»Ώ net8.0;net9.0 @@ -17,6 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + From dd1c8e4f78bf17b5fdb36a070a517a5fff0546d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:48:13 -0500 Subject: [PATCH 233/316] chore(deps): update dependency fluentassertions to 7.1.0 (#346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [FluentAssertions](https://xceed.com/products/unit-testing/fluent-assertions/) ([source](https://redirect.github.com/fluentassertions/fluentassertions)) | `7.0.0` -> `7.1.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/FluentAssertions/7.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/FluentAssertions/7.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/FluentAssertions/7.0.0/7.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/FluentAssertions/7.0.0/7.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
fluentassertions/fluentassertions (FluentAssertions) ### [`v7.1.0`](https://redirect.github.com/fluentassertions/fluentassertions/releases/tag/7.1.0) [Compare Source](https://redirect.github.com/fluentassertions/fluentassertions/compare/7.0.0...7.1.0) ##### What's Changed ##### Improvements - Backport TUnit to v7 by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2971](https://redirect.github.com/fluentassertions/fluentassertions/pull/2971) ##### Fixes - Backport xUnit 3 support by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2970](https://redirect.github.com/fluentassertions/fluentassertions/pull/2970) ##### Others - Bump all dependencies by [@​dennisdoomen](https://redirect.github.com/dennisdoomen) in [https://github.com/fluentassertions/fluentassertions/pull/2962](https://redirect.github.com/fluentassertions/fluentassertions/pull/2962) **Full Changelog**: https://github.com/fluentassertions/fluentassertions/compare/7.0.0...7.1.0
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e83d449a..38878dcd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + From 8b36d68e204b7bdd92cc4bc779ca7b591456fa6a Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Fri, 31 Jan 2025 09:18:33 -0500 Subject: [PATCH 234/316] ci: add dco workaround (#357) Signed-off-by: Michael Beemer --- .github/workflows/dco-merge-group.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/dco-merge-group.yml diff --git a/.github/workflows/dco-merge-group.yml b/.github/workflows/dco-merge-group.yml new file mode 100644 index 00000000..0241f80a --- /dev/null +++ b/.github/workflows/dco-merge-group.yml @@ -0,0 +1,12 @@ +name: DCO +on: + merge_group: + +# Workaround because the DCO app doesn't run on a merge_group trigger +# https://github.com/dcoapp/app/pull/200 +jobs: + DCO: + runs-on: ubuntu-latest + 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" From fb7b36b6a3333d9f7cf08771c0d89e6d3690db4e Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Fri, 31 Jan 2025 10:26:27 -0500 Subject: [PATCH 235/316] ci: run e2e for merge groups (#360) Signed-off-by: Michael Beemer --- .github/workflows/e2e.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0be1db57..80c538e0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,14 +1,14 @@ name: E2E Test on: - push: - branches: [ main ] - paths-ignore: - - '**.md' pull_request: - branches: [ main ] - paths-ignore: - - '**.md' + types: + - opened + - synchronize + - reopened + branches: + - main + merge_group: jobs: e2e-tests: From 1e8ebc447f5f0d76cfb6e03d034d663ae0c32830 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:26:37 +0000 Subject: [PATCH 236/316] chore(deps): update codecov/codecov-action action to v5.3.1 (#355) 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://redirect.github.com/codecov/codecov-action) | action | minor | `v5.1.2` -> `v5.3.1` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v5.3.1`](https://redirect.github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v531) [Compare Source](https://redirect.github.com/codecov/codecov-action/compare/v5.3.0...v5.3.1) ##### What's Changed **Full Changelog**: https://github.com/codecov/codecov-action/compare/v5.3.0..v5.3.1 ### [`v5.3.0`](https://redirect.github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v530) [Compare Source](https://redirect.github.com/codecov/codecov-action/compare/v5.2.0...v5.3.0) ##### What's Changed **Full Changelog**: https://github.com/codecov/codecov-action/compare/v5.2.0..v5.3.0 ### [`v5.2.0`](https://redirect.github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v520) [Compare Source](https://redirect.github.com/codecov/codecov-action/compare/v5.1.2...v5.2.0) ##### What's Changed - Fix typo in README by [@​tserg](https://redirect.github.com/tserg) in [https://github.com/codecov/codecov-action/pull/1747](https://redirect.github.com/codecov/codecov-action/pull/1747) - Th/add commands by [@​thomasrockhu-codecov](https://redirect.github.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1745](https://redirect.github.com/codecov/codecov-action/pull/1745) - use correct audience when requesting oidc token by [@​juho9000](https://redirect.github.com/juho9000) in [https://github.com/codecov/codecov-action/pull/1744](https://redirect.github.com/codecov/codecov-action/pull/1744) - build(deps): bump github/codeql-action from 3.27.9 to 3.28.1 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1742](https://redirect.github.com/codecov/codecov-action/pull/1742) - build(deps): bump actions/upload-artifact from 4.4.3 to 4.6.0 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1743](https://redirect.github.com/codecov/codecov-action/pull/1743) - chore(deps): bump wrapper to 0.0.32 by [@​thomasrockhu-codecov](https://redirect.github.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1740](https://redirect.github.com/codecov/codecov-action/pull/1740) - feat: add disable-telem feature by [@​thomasrockhu-codecov](https://redirect.github.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1739](https://redirect.github.com/codecov/codecov-action/pull/1739) - fix: remove erroneous linebreak in readme by [@​Vampire](https://redirect.github.com/Vampire) in [https://github.com/codecov/codecov-action/pull/1734](https://redirect.github.com/codecov/codecov-action/pull/1734) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v5.1.2..v5.2.0
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **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 bbfd853a..ff5e77ab 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -36,7 +36,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v5.1.2 + - uses: codecov/codecov-action@v5.3.1 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From 83d790111fde3872e72bb4a727700b8c90ab0888 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Fri, 31 Jan 2025 10:31:20 -0500 Subject: [PATCH 237/316] ci: update release please config Use the OpenFeature Bot user Signed-off-by: Michael Beemer --- .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 050024d8..e5f38efa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,9 @@ jobs: id: release with: command: manifest - token: ${{secrets.GITHUB_TOKEN}} + token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}} default-branch: main + signoff: "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>" outputs: release_created: ${{ steps.release.outputs.release_created }} release_tag_name: ${{ steps.release.outputs.tag_name }} From 0cf5834cbaefc8e2ed144e821d0f8f7fce4b4771 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:52:31 -0500 Subject: [PATCH 238/316] chore(main): release 2.3.0 (#330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.3.0](https://github.com/open-feature/dotnet-sdk/compare/v2.2.0...v2.3.0) (2025-01-31) ### ⚠ BREAKING CHANGES #### Hook Changes The signature of the `finally` hook stage has been changed. The signature now includes the `evaluation details`, as per the [OpenFeature specification](https://openfeature.dev/specification/sections/hooks#requirement-438). Note that since hooks are still `experimental,` this does not constitute a change requiring a new major version. To migrate, update any hook that implements the `finally` stage to accept `evaluation details` as the second argument. * Add evaluation details to finally hook stage ([#335](https://github.com/open-feature/dotnet-sdk/issues/335)) ([2ef9955](https://github.com/open-feature/dotnet-sdk/commit/2ef995529d377826d467fa486f18af20bfeeba60)) #### .NET 6 Removed support for .NET 6. * add dotnet 9 support, rm dotnet 6 ([#317](https://github.com/open-feature/dotnet-sdk/issues/317)) ([2774b0d](https://github.com/open-feature/dotnet-sdk/commit/2774b0d3c09f2f206834ca3fe2526e3eb3ca8087)) ### πŸ› Bug Fixes * Adding Async Lifetime method to fix flaky unit tests ([#333](https://github.com/open-feature/dotnet-sdk/issues/333)) ([e14ab39](https://github.com/open-feature/dotnet-sdk/commit/e14ab39180d38544132e9fe92244b7b37255d2cf)) * Fix issue with DI documentation ([#350](https://github.com/open-feature/dotnet-sdk/issues/350)) ([728ae47](https://github.com/open-feature/dotnet-sdk/commit/728ae471625ab1ff5f166b60a5830afbaf9ad276)) ### ✨ New Features * add dotnet 9 support, rm dotnet 6 ([#317](https://github.com/open-feature/dotnet-sdk/issues/317)) ([2774b0d](https://github.com/open-feature/dotnet-sdk/commit/2774b0d3c09f2f206834ca3fe2526e3eb3ca8087)) * Add evaluation details to finally hook stage ([#335](https://github.com/open-feature/dotnet-sdk/issues/335)) ([2ef9955](https://github.com/open-feature/dotnet-sdk/commit/2ef995529d377826d467fa486f18af20bfeeba60)) * Implement Default Logging Hook ([#308](https://github.com/open-feature/dotnet-sdk/issues/308)) ([7013e95](https://github.com/open-feature/dotnet-sdk/commit/7013e9503f6721bd5f241c6c4d082a4a4e9eceed)) * Implement transaction context ([#312](https://github.com/open-feature/dotnet-sdk/issues/312)) ([1b5a0a9](https://github.com/open-feature/dotnet-sdk/commit/1b5a0a9823e4f68e9356536ad5aa8418d8ca815f)) ### 🧹 Chore * **deps:** update actions/upload-artifact action to v4.5.0 ([#332](https://github.com/open-feature/dotnet-sdk/issues/332)) ([fd68cb0](https://github.com/open-feature/dotnet-sdk/commit/fd68cb0bed0228607cc2369ef6822dd518c5fbec)) * **deps:** update codecov/codecov-action action to v5 ([#316](https://github.com/open-feature/dotnet-sdk/issues/316)) ([6c4cd02](https://github.com/open-feature/dotnet-sdk/commit/6c4cd0273f85bc0be0b07753d47bf13a613bbf82)) * **deps:** update codecov/codecov-action action to v5.1.2 ([#334](https://github.com/open-feature/dotnet-sdk/issues/334)) ([b9ebddf](https://github.com/open-feature/dotnet-sdk/commit/b9ebddfccb094f45a50e8196e43c087b4e97ffa4)) * **deps:** update codecov/codecov-action action to v5.3.1 ([#355](https://github.com/open-feature/dotnet-sdk/issues/355)) ([1e8ebc4](https://github.com/open-feature/dotnet-sdk/commit/1e8ebc447f5f0d76cfb6e03d034d663ae0c32830)) * **deps:** update dependency coverlet.collector to 6.0.3 ([#336](https://github.com/open-feature/dotnet-sdk/issues/336)) ([8527b03](https://github.com/open-feature/dotnet-sdk/commit/8527b03fb020a9604463da80f305978baa85f172)) * **deps:** update dependency coverlet.msbuild to 6.0.3 ([#337](https://github.com/open-feature/dotnet-sdk/issues/337)) ([26fd235](https://github.com/open-feature/dotnet-sdk/commit/26fd2356c1835271dee2f7b8b03b2c83e9cb2eea)) * **deps:** update dependency dotnet-sdk to v9.0.101 ([#339](https://github.com/open-feature/dotnet-sdk/issues/339)) ([dd26ad6](https://github.com/open-feature/dotnet-sdk/commit/dd26ad6d35e134ab40a290e644d5f8bdc8e56c66)) * **deps:** update dependency fluentassertions to 7.1.0 ([#346](https://github.com/open-feature/dotnet-sdk/issues/346)) ([dd1c8e4](https://github.com/open-feature/dotnet-sdk/commit/dd1c8e4f78bf17b5fdb36a070a517a5fff0546d2)) * **deps:** update dependency microsoft.net.test.sdk to 17.12.0 ([#322](https://github.com/open-feature/dotnet-sdk/issues/322)) ([6f5b049](https://github.com/open-feature/dotnet-sdk/commit/6f5b04997aee44c2023e75471932e9f5ff27b0be)) ### πŸ“š Documentation * disable space in link text lint rule ([#329](https://github.com/open-feature/dotnet-sdk/issues/329)) ([583b2a9](https://github.com/open-feature/dotnet-sdk/commit/583b2a9beab18ba70f8789b903d61a4c685560f0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Signed-off-by: Todd Baert Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Todd Baert --- .release-please-manifest.json | 2 +- CHANGELOG.md | 46 +++++++++++++++++++++++++++++++++++ README.md | 4 +-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a5d1cf28..9965a341 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.0" + ".": "2.3.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a460be..f07a6552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## [2.3.0](https://github.com/open-feature/dotnet-sdk/compare/v2.2.0...v2.3.0) (2025-01-31) + + +#### Hook Changes + +The signature of the `finally` hook stage has been changed. The signature now includes the `evaluation details`, as per the [OpenFeature specification](https://openfeature.dev/specification/sections/hooks#requirement-438). Note that since hooks are still `experimental,` this does not constitute a change requiring a new major version. To migrate, update any hook that implements the `finally` stage to accept `evaluation details` as the second argument. + +* Add evaluation details to finally hook stage ([#335](https://github.com/open-feature/dotnet-sdk/issues/335)) ([2ef9955](https://github.com/open-feature/dotnet-sdk/commit/2ef995529d377826d467fa486f18af20bfeeba60)) + +#### .NET 6 + +Removed support for .NET 6. + +* add dotnet 9 support, rm dotnet 6 ([#317](https://github.com/open-feature/dotnet-sdk/issues/317)) ([2774b0d](https://github.com/open-feature/dotnet-sdk/commit/2774b0d3c09f2f206834ca3fe2526e3eb3ca8087)) + +### πŸ› Bug Fixes + +* Adding Async Lifetime method to fix flaky unit tests ([#333](https://github.com/open-feature/dotnet-sdk/issues/333)) ([e14ab39](https://github.com/open-feature/dotnet-sdk/commit/e14ab39180d38544132e9fe92244b7b37255d2cf)) +* Fix issue with DI documentation ([#350](https://github.com/open-feature/dotnet-sdk/issues/350)) ([728ae47](https://github.com/open-feature/dotnet-sdk/commit/728ae471625ab1ff5f166b60a5830afbaf9ad276)) + + +### ✨ New Features + +* add dotnet 9 support, rm dotnet 6 ([#317](https://github.com/open-feature/dotnet-sdk/issues/317)) ([2774b0d](https://github.com/open-feature/dotnet-sdk/commit/2774b0d3c09f2f206834ca3fe2526e3eb3ca8087)) +* Add evaluation details to finally hook stage ([#335](https://github.com/open-feature/dotnet-sdk/issues/335)) ([2ef9955](https://github.com/open-feature/dotnet-sdk/commit/2ef995529d377826d467fa486f18af20bfeeba60)) +* Implement Default Logging Hook ([#308](https://github.com/open-feature/dotnet-sdk/issues/308)) ([7013e95](https://github.com/open-feature/dotnet-sdk/commit/7013e9503f6721bd5f241c6c4d082a4a4e9eceed)) +* Implement transaction context ([#312](https://github.com/open-feature/dotnet-sdk/issues/312)) ([1b5a0a9](https://github.com/open-feature/dotnet-sdk/commit/1b5a0a9823e4f68e9356536ad5aa8418d8ca815f)) + + +### 🧹 Chore + +* **deps:** update actions/upload-artifact action to v4.5.0 ([#332](https://github.com/open-feature/dotnet-sdk/issues/332)) ([fd68cb0](https://github.com/open-feature/dotnet-sdk/commit/fd68cb0bed0228607cc2369ef6822dd518c5fbec)) +* **deps:** update codecov/codecov-action action to v5 ([#316](https://github.com/open-feature/dotnet-sdk/issues/316)) ([6c4cd02](https://github.com/open-feature/dotnet-sdk/commit/6c4cd0273f85bc0be0b07753d47bf13a613bbf82)) +* **deps:** update codecov/codecov-action action to v5.1.2 ([#334](https://github.com/open-feature/dotnet-sdk/issues/334)) ([b9ebddf](https://github.com/open-feature/dotnet-sdk/commit/b9ebddfccb094f45a50e8196e43c087b4e97ffa4)) +* **deps:** update codecov/codecov-action action to v5.3.1 ([#355](https://github.com/open-feature/dotnet-sdk/issues/355)) ([1e8ebc4](https://github.com/open-feature/dotnet-sdk/commit/1e8ebc447f5f0d76cfb6e03d034d663ae0c32830)) +* **deps:** update dependency coverlet.collector to 6.0.3 ([#336](https://github.com/open-feature/dotnet-sdk/issues/336)) ([8527b03](https://github.com/open-feature/dotnet-sdk/commit/8527b03fb020a9604463da80f305978baa85f172)) +* **deps:** update dependency coverlet.msbuild to 6.0.3 ([#337](https://github.com/open-feature/dotnet-sdk/issues/337)) ([26fd235](https://github.com/open-feature/dotnet-sdk/commit/26fd2356c1835271dee2f7b8b03b2c83e9cb2eea)) +* **deps:** update dependency dotnet-sdk to v9.0.101 ([#339](https://github.com/open-feature/dotnet-sdk/issues/339)) ([dd26ad6](https://github.com/open-feature/dotnet-sdk/commit/dd26ad6d35e134ab40a290e644d5f8bdc8e56c66)) +* **deps:** update dependency fluentassertions to 7.1.0 ([#346](https://github.com/open-feature/dotnet-sdk/issues/346)) ([dd1c8e4](https://github.com/open-feature/dotnet-sdk/commit/dd1c8e4f78bf17b5fdb36a070a517a5fff0546d2)) +* **deps:** update dependency microsoft.net.test.sdk to 17.12.0 ([#322](https://github.com/open-feature/dotnet-sdk/issues/322)) ([6f5b049](https://github.com/open-feature/dotnet-sdk/commit/6f5b04997aee44c2023e75471932e9f5ff27b0be)) + + +### πŸ“š Documentation + +* disable space in link text lint rule ([#329](https://github.com/open-feature/dotnet-sdk/issues/329)) ([583b2a9](https://github.com/open-feature/dotnet-sdk/commit/583b2a9beab18ba70f8789b903d61a4c685560f0)) + ## [2.2.0](https://github.com/open-feature/dotnet-sdk/compare/v2.1.0...v2.2.0) (2024-12-12) diff --git a/README.md b/README.md index 857eae25..eae48181 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v2.2.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.2.0) + ![Release](https://img.shields.io/static/v1?label=release&message=v2.3.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index f17d78d5..624a7f50 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@
- 2.2.0 + 2.3.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index ccbccc3d..276cbf9e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.2.0 +2.3.0 From 32dab9ba0904a6b27fc53e21402ae95d5594ad01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:20:47 -0500 Subject: [PATCH 239/316] chore(deps): update dotnet monorepo (#343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | Type | Update | |---|---|---|---|---|---|---|---| | [Microsoft.AspNetCore.TestHost](https://asp.net/) ([source](https://redirect.github.com/dotnet/aspnetcore)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.AspNetCore.TestHost/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.AspNetCore.TestHost/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.AspNetCore.TestHost/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.AspNetCore.TestHost/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Bcl.AsyncInterfaces](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.DependencyInjection](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.DependencyInjection/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.DependencyInjection/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.DependencyInjection/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.DependencyInjection/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.DependencyInjection.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.Diagnostics.Testing](https://dot.net/) ([source](https://redirect.github.com/dotnet/extensions)) | `9.0.0` -> `9.1.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Diagnostics.Testing/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Diagnostics.Testing/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Diagnostics.Testing/9.0.0/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Diagnostics.Testing/9.0.0/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | minor | | [Microsoft.Extensions.Hosting.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.Logging.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.Options](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Options/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Options/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Options/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Options/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [System.Collections.Immutable](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/System.Collections.Immutable/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/System.Collections.Immutable/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/System.Collections.Immutable/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/System.Collections.Immutable/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [System.Threading.Channels](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.0` -> `9.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/System.Threading.Channels/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/System.Threading.Channels/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/System.Threading.Channels/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/System.Threading.Channels/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [dotnet-sdk](https://redirect.github.com/dotnet/sdk) | `9.0.101` -> `9.0.102` | [![age](https://developer.mend.io/api/mc/badges/age/dotnet-version/dotnet-sdk/9.0.102?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/dotnet-version/dotnet-sdk/9.0.102?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/dotnet-version/dotnet-sdk/9.0.101/9.0.102?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/dotnet-version/dotnet-sdk/9.0.101/9.0.102?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dotnet-sdk | patch | --- ### Release Notes
dotnet/aspnetcore (Microsoft.AspNetCore.TestHost) ### [`v9.0.1`](https://redirect.github.com/dotnet/aspnetcore/releases/tag/v9.0.1): .NET 9.0.1 [Release](https://redirect.github.com/dotnet/core/releases/tag/v9.0.1) ##### What's Changed - Merging internal commits for release/9.0 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/aspnetcore/pull/58900](https://redirect.github.com/dotnet/aspnetcore/pull/58900) - \[release/9.0] Prevent unnecessary debugger stops for user-unhandled exceptions in Blazor apps with Just My Code enabled by [@​halter73](https://redirect.github.com/halter73) in [https://github.com/dotnet/aspnetcore/pull/58573](https://redirect.github.com/dotnet/aspnetcore/pull/58573) - Hot Reload agent improvements by [@​tmat](https://redirect.github.com/tmat) in [https://github.com/dotnet/aspnetcore/pull/58333](https://redirect.github.com/dotnet/aspnetcore/pull/58333) - \[release/9.0] Update dependencies from roslyn by [@​wtgodbe](https://redirect.github.com/wtgodbe) in [https://github.com/dotnet/aspnetcore/pull/59183](https://redirect.github.com/dotnet/aspnetcore/pull/59183) - \[release/9.0] Add direct reference to System.Drawing.Common in tools by [@​wtgodbe](https://redirect.github.com/wtgodbe) in [https://github.com/dotnet/aspnetcore/pull/59189](https://redirect.github.com/dotnet/aspnetcore/pull/59189) - \[release/9.0] Harden parsing of \[Range] attribute values by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/59077](https://redirect.github.com/dotnet/aspnetcore/pull/59077) - \[release/9.0] Update dependencies from dotnet/source-build-externals by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/aspnetcore/pull/59143](https://redirect.github.com/dotnet/aspnetcore/pull/59143) - \[release/9.0] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/aspnetcore/pull/59024](https://redirect.github.com/dotnet/aspnetcore/pull/59024) - \[release/9.0] (deps): Bump src/submodules/googletest from `6dae7eb` to `d144031` by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/dotnet/aspnetcore/pull/59032](https://redirect.github.com/dotnet/aspnetcore/pull/59032) - \[release/9.0] Update dependencies from dotnet/xdt by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/aspnetcore/pull/58589](https://redirect.github.com/dotnet/aspnetcore/pull/58589) - \[release/9.0] Update dependencies from dotnet/extensions by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/aspnetcore/pull/58675](https://redirect.github.com/dotnet/aspnetcore/pull/58675) - \[release/9.0] Fix SignalR Java POM to include description by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/58896](https://redirect.github.com/dotnet/aspnetcore/pull/58896) - \[release/9.0] Fix IIS outofprocess to remove WebSocket compression handshake by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/58931](https://redirect.github.com/dotnet/aspnetcore/pull/58931) **Full Changelog**: https://github.com/dotnet/aspnetcore/compare/v9.0.0...v9.0.1
dotnet/runtime (Microsoft.Bcl.AsyncInterfaces) ### [`v9.0.1`](https://redirect.github.com/dotnet/runtime/releases/tag/v9.0.1): .NET 9.0.1 [Release](https://redirect.github.com/dotnet/core/releases/tag/v9.0.1) ##### What's Changed - \[release/9.0-staging] Upgrade our macOS build machines to the latest non-beta x64 image by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109455](https://redirect.github.com/dotnet/runtime/pull/109455) - \[release/9.0-staging] Remove thread contention from Activity Start/Stop by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109359](https://redirect.github.com/dotnet/runtime/pull/109359) - \[release/9.0-staging] handle case of Proc Index > MAX_SUPPORTED_CPUS by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109385](https://redirect.github.com/dotnet/runtime/pull/109385) - Update branding to 9.0.1 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/runtime/pull/109563](https://redirect.github.com/dotnet/runtime/pull/109563) - \[release/9.0-staging] \[android] Fix crash in method_to_ir by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109510](https://redirect.github.com/dotnet/runtime/pull/109510) - \[release/9.0-staging] Switch to non-incremental servicing by [@​carlossanlop](https://redirect.github.com/carlossanlop) in [https://github.com/dotnet/runtime/pull/109316](https://redirect.github.com/dotnet/runtime/pull/109316) - \[release/9.0] \[wasm] Use correct current runtime pack version for Wasm.Build.Tests by [@​maraf](https://redirect.github.com/maraf) in [https://github.com/dotnet/runtime/pull/109820](https://redirect.github.com/dotnet/runtime/pull/109820) - \[release/9.0-staging] Update ApiCompatNetCoreAppBaselineVersion to 9.0.0 by [@​carlossanlop](https://redirect.github.com/carlossanlop) in [https://github.com/dotnet/runtime/pull/109789](https://redirect.github.com/dotnet/runtime/pull/109789) - \[release/9.0] \[wasm] Run downlevel tests only on main by [@​maraf](https://redirect.github.com/maraf) in [https://github.com/dotnet/runtime/pull/109723](https://redirect.github.com/dotnet/runtime/pull/109723) - \[release/9.0-staging] Fix regression in constructor parameter binding logic. by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109813](https://redirect.github.com/dotnet/runtime/pull/109813) - \[release/9.0-staging] `TensorPrimitives` XML docs: `MinNumber`/`ReciprocalSqrt`/`ReciprocalSqrtEstimate` oversights by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109922](https://redirect.github.com/dotnet/runtime/pull/109922) - \[release/9.0-staging] Add a missing = in BigInteger.cs by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109732](https://redirect.github.com/dotnet/runtime/pull/109732) - \[release/9.0-staging] Ignore modopts/modreqs for `UnsafeAccessor` field targets by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109709](https://redirect.github.com/dotnet/runtime/pull/109709) - Fix an issue with sysconf returning the wrong last level cache values on Linux running on certain AMD Processors. by [@​mrsharm](https://redirect.github.com/mrsharm) in [https://github.com/dotnet/runtime/pull/109749](https://redirect.github.com/dotnet/runtime/pull/109749) - \[release/9.0-staging] Fix transformer handling of boolean schemas in JsonSchemaExporter. by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109975](https://redirect.github.com/dotnet/runtime/pull/109975) - \[release/9.0-staging] Ensure proper cleanup of key files when not persisting them by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109844](https://redirect.github.com/dotnet/runtime/pull/109844) - \[release/9.0-staging] Transfer ThreadPool local queue to high-pri queue on Task blocking by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/109989](https://redirect.github.com/dotnet/runtime/pull/109989) - Merging internal commits for release/9.0 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/runtime/pull/109744](https://redirect.github.com/dotnet/runtime/pull/109744) - \[release/9.0-staging] DATAS BGC thread synchronization fix by [@​Maoni0](https://redirect.github.com/Maoni0) in [https://github.com/dotnet/runtime/pull/110174](https://redirect.github.com/dotnet/runtime/pull/110174) - \[release/9.0-staging] Fix Matrix4x4.CreateReflection when D is not zero by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/110162](https://redirect.github.com/dotnet/runtime/pull/110162) - \[release/9.0-staging] Fix hostfxr.h to be valid C again. by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/110060](https://redirect.github.com/dotnet/runtime/pull/110060) - \[release/9.0] Fix length check for Convert.TryToHexString{Lower} by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/110228](https://redirect.github.com/dotnet/runtime/pull/110228) - \[release/9.0-staging] Suppress IL3050 warnings in ILLink tests by [@​sbomer](https://redirect.github.com/sbomer) in [https://github.com/dotnet/runtime/pull/110340](https://redirect.github.com/dotnet/runtime/pull/110340) - \[release/9.0-staging] Update Azure Linux tag names by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/110341](https://redirect.github.com/dotnet/runtime/pull/110341) - \[release/9.0-staging] Update dependencies from dotnet/icu by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109299](https://redirect.github.com/dotnet/runtime/pull/109299) - \[release/9.0-staging] Update dependencies from dotnet/xharness by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109306](https://redirect.github.com/dotnet/runtime/pull/109306) - \[release/9.0-staging] Update dependencies from dotnet/cecil by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109297](https://redirect.github.com/dotnet/runtime/pull/109297) - \[release/9.0-staging] Update dependencies from dotnet/source-build-reference-packages by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109301](https://redirect.github.com/dotnet/runtime/pull/109301) - \[release/9.0-staging] Update dependencies from dotnet/roslyn-analyzers by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109303](https://redirect.github.com/dotnet/runtime/pull/109303) - \[release/9.0-staging] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109305](https://redirect.github.com/dotnet/runtime/pull/109305) - \[release/9.0-staging] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109825](https://redirect.github.com/dotnet/runtime/pull/109825) - \[release/9.0-staging] Update dependencies from dotnet/source-build-externals by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109960](https://redirect.github.com/dotnet/runtime/pull/109960) - \[release/9.0-staging] Update dependencies from dotnet/sdk by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109304](https://redirect.github.com/dotnet/runtime/pull/109304) - \[release/9.0-staging] Update dependencies from dotnet/emsdk by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109298](https://redirect.github.com/dotnet/runtime/pull/109298) - \[release/9.0-staging] Update dependencies from dotnet/runtime-assets by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109336](https://redirect.github.com/dotnet/runtime/pull/109336) - \[release/9.0] Update dependencies from dotnet/emsdk by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109523](https://redirect.github.com/dotnet/runtime/pull/109523) - \[release/9.0-staging] Update dependencies from dotnet/hotreload-utils by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/109308](https://redirect.github.com/dotnet/runtime/pull/109308) - \[manual] Merge release/9.0-staging into release/9.0 by [@​carlossanlop](https://redirect.github.com/carlossanlop) in [https://github.com/dotnet/runtime/pull/110370](https://redirect.github.com/dotnet/runtime/pull/110370) - Switch to automatic 8.0 version updates by [@​marcpopMSFT](https://redirect.github.com/marcpopMSFT) in [https://github.com/dotnet/runtime/pull/110586](https://redirect.github.com/dotnet/runtime/pull/110586) - \[release/9.0] Update dependencies from dotnet/emsdk by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/110409](https://redirect.github.com/dotnet/runtime/pull/110409) **Full Changelog**: https://github.com/dotnet/runtime/compare/v9.0.0...v9.0.1
dotnet/extensions (Microsoft.Extensions.Diagnostics.Testing) ### [`v9.1.0`](https://redirect.github.com/dotnet/extensions/releases/tag/v9.1.0): .NET Extensions 9.1.0 9.1.0 packages are all pushed now to NuGet.org! #### What's Changed - Add NativeAOT testapp project for M.E.AI by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5573](https://redirect.github.com/dotnet/extensions/pull/5573) - Add changelogs for M.E.AI projects by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5577](https://redirect.github.com/dotnet/extensions/pull/5577) - Explicitly reference System.Memory.Data in OpenAI/AzureAIInference projects by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5576](https://redirect.github.com/dotnet/extensions/pull/5576) - Fix AzureAIInferenceEmbeddingGenerator to respect EmbeddingGenerationOptions.Dimensions by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5575](https://redirect.github.com/dotnet/extensions/pull/5575) - Merge ResourceMonitoringOptions.Linux.cs into ResourceMonitoringOptions.cs by [@​makazeu](https://redirect.github.com/makazeu) in [https://github.com/dotnet/extensions/pull/5580](https://redirect.github.com/dotnet/extensions/pull/5580) - Fix exception when generating boolean schemas by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5585](https://redirect.github.com/dotnet/extensions/pull/5585) - Add ImageContent integration test by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5586](https://redirect.github.com/dotnet/extensions/pull/5586) - Add ChatOptions.Seed by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5587](https://redirect.github.com/dotnet/extensions/pull/5587) - Lower `AIJsonUtilities` to STJv8 and move to Abstractions library. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5582](https://redirect.github.com/dotnet/extensions/pull/5582) - Plug JsonSchemaExporter test data to the AIJsonUtilities tests by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5590](https://redirect.github.com/dotnet/extensions/pull/5590) - Improve JsonSchemaExporter trimmer safety. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5591](https://redirect.github.com/dotnet/extensions/pull/5591) - Improve AdditionalPropertiesDictionary by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5593](https://redirect.github.com/dotnet/extensions/pull/5593) - Add UseEmbeddingGenerationOptions by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5594](https://redirect.github.com/dotnet/extensions/pull/5594) - HybridCache stability and logging improvements by [@​mgravell](https://redirect.github.com/mgravell) in [https://github.com/dotnet/extensions/pull/5467](https://redirect.github.com/dotnet/extensions/pull/5467) - Assign ownership by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5600](https://redirect.github.com/dotnet/extensions/pull/5600) - HybridCache: don't log cancellation as failure event by [@​mgravell](https://redirect.github.com/mgravell) in [https://github.com/dotnet/extensions/pull/5601](https://redirect.github.com/dotnet/extensions/pull/5601) - Set DisableNETStandardCompatErrors for M.E.AI projects by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5603](https://redirect.github.com/dotnet/extensions/pull/5603) - HttpRouteParser: Handle catch-all parameters by [@​dariusclay](https://redirect.github.com/dariusclay) in [https://github.com/dotnet/extensions/pull/5604](https://redirect.github.com/dotnet/extensions/pull/5604) - Rework UseChatOptions as ConfigureOptions by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5606](https://redirect.github.com/dotnet/extensions/pull/5606) - Make IChatClient/IEmbeddingGenerator.GetService non-generic by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5608](https://redirect.github.com/dotnet/extensions/pull/5608) - Add logging/activities to FunctionInvokingChatClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5596](https://redirect.github.com/dotnet/extensions/pull/5596) - Update M.E.AI CHANGELOG.mds by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5609](https://redirect.github.com/dotnet/extensions/pull/5609) - \[main] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5610](https://redirect.github.com/dotnet/extensions/pull/5610) - Add ToChatCompletion{Async} methods for combining StreamingChatCompletionUpdates by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5605](https://redirect.github.com/dotnet/extensions/pull/5605) - Docs improvements by [@​gewarren](https://redirect.github.com/gewarren) in [https://github.com/dotnet/extensions/pull/5613](https://redirect.github.com/dotnet/extensions/pull/5613) - Use ToChatCompletion in OpenTelemetryChatClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5614](https://redirect.github.com/dotnet/extensions/pull/5614) - Use ToChatCompletion / ToStreamingChatCompletionUpdates in CachingChatClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5616](https://redirect.github.com/dotnet/extensions/pull/5616) - Add DebuggerDisplay for DataContent by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5618](https://redirect.github.com/dotnet/extensions/pull/5618) - Tweak ChatMessage/StreamingChatCompletionUpdate.ToString by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5617](https://redirect.github.com/dotnet/extensions/pull/5617) - Expose options for making schema generation conformant with the subset accepted by OpenAI. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5619](https://redirect.github.com/dotnet/extensions/pull/5619) - Cache current process object to avoid performance hit by [@​haipz](https://redirect.github.com/haipz) in [https://github.com/dotnet/extensions/pull/5597](https://redirect.github.com/dotnet/extensions/pull/5597) - Fix namespace for IServiceCollection extensions by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5620](https://redirect.github.com/dotnet/extensions/pull/5620) - Fix linker warning. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5627](https://redirect.github.com/dotnet/extensions/pull/5627) - Merge internal changes by [@​joperezr](https://redirect.github.com/joperezr) in [https://github.com/dotnet/extensions/pull/5631](https://redirect.github.com/dotnet/extensions/pull/5631) - Publish the AotCompatibility.TestApp project as part of PR validation by [@​eerhardt](https://redirect.github.com/eerhardt) in [https://github.com/dotnet/extensions/pull/5622](https://redirect.github.com/dotnet/extensions/pull/5622) - Merge branch release/9.0 into main by [@​joperezr](https://redirect.github.com/joperezr) in [https://github.com/dotnet/extensions/pull/5632](https://redirect.github.com/dotnet/extensions/pull/5632) - Replace STJ boilerplate in the leaf clients with AIJsonUtilities calls. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5630](https://redirect.github.com/dotnet/extensions/pull/5630) - Rework cache key handling in caching client / generator by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5641](https://redirect.github.com/dotnet/extensions/pull/5641) - Docs updates by [@​gewarren](https://redirect.github.com/gewarren) in [https://github.com/dotnet/extensions/pull/5643](https://redirect.github.com/dotnet/extensions/pull/5643) - Change ChatClientBuilder to register singletons and support lambda-less chaining by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5642](https://redirect.github.com/dotnet/extensions/pull/5642) - \[main] Update dependencies from dotnet/aspnetcore by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5645](https://redirect.github.com/dotnet/extensions/pull/5645) - EmbeddingGeneratorBuilder API updates by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5647](https://redirect.github.com/dotnet/extensions/pull/5647) - Update WaiterRemovedAfterDispose to check waitersCount first by [@​amadeuszl](https://redirect.github.com/amadeuszl) in [https://github.com/dotnet/extensions/pull/5646](https://redirect.github.com/dotnet/extensions/pull/5646) - Allow logging of body without modifying the actual response by [@​dariusclay](https://redirect.github.com/dariusclay) in [https://github.com/dotnet/extensions/pull/5628](https://redirect.github.com/dotnet/extensions/pull/5628) - Make ActivityBaggageLogScopeWrapper implements IEnumerable\> by [@​NatMarchand](https://redirect.github.com/NatMarchand) in [https://github.com/dotnet/extensions/pull/5589](https://redirect.github.com/dotnet/extensions/pull/5589) - Add a \[DebuggerDisplay] to GeneratedEmbeddings by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5657](https://redirect.github.com/dotnet/extensions/pull/5657) - Annotate private DebuggerDisplay props as DebuggerBrowsableState.Never by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5656](https://redirect.github.com/dotnet/extensions/pull/5656) - Fix M.E.AI argument tests to validate argument names by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5653](https://redirect.github.com/dotnet/extensions/pull/5653) - Remove duplicate GetCacheKey methods by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5651](https://redirect.github.com/dotnet/extensions/pull/5651) - Augment XML comments for AIFunctionFactory.Create by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5658](https://redirect.github.com/dotnet/extensions/pull/5658) - \[main] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5662](https://redirect.github.com/dotnet/extensions/pull/5662) - Add AsBuilder extensions for IChatClient and IEmbeddingGenerator by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5652](https://redirect.github.com/dotnet/extensions/pull/5652) - Reduce a bit of LINQ in M.E.AI by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5663](https://redirect.github.com/dotnet/extensions/pull/5663) - Reverse order of services/inner in Use methods by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5664](https://redirect.github.com/dotnet/extensions/pull/5664) - Add anonymous chat clients / embedding generators by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5650](https://redirect.github.com/dotnet/extensions/pull/5650) - Update documentation SynchronizationContext in FakeTimeProvider by [@​amadeuszl](https://redirect.github.com/amadeuszl) in [https://github.com/dotnet/extensions/pull/5665](https://redirect.github.com/dotnet/extensions/pull/5665) - Backport JsonSchemaExporter bugfix. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5671](https://redirect.github.com/dotnet/extensions/pull/5671) - Add OpenAIRealtimeExtensions with ToConversationFunctionTool by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5666](https://redirect.github.com/dotnet/extensions/pull/5666) - Tweak CachingHelpers.GetCacheKey to clean up better on failure by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5654](https://redirect.github.com/dotnet/extensions/pull/5654) - Ensure non-streaming usage data from function calling is in history by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5676](https://redirect.github.com/dotnet/extensions/pull/5676) - Fix a few FunctionInvocationChatClient streaming issues by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5680](https://redirect.github.com/dotnet/extensions/pull/5680) - Change UseLogging to accept an ILoggerFactory instead of ILogger by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5682](https://redirect.github.com/dotnet/extensions/pull/5682) - Expose a schema transformer on AIJsonSchemaCreateOptions. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5677](https://redirect.github.com/dotnet/extensions/pull/5677) - Update M.E.AI CHANGELOG.md for latest bits by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5684](https://redirect.github.com/dotnet/extensions/pull/5684) - \[main] Update dependencies from dotnet/aspnetcore by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5678](https://redirect.github.com/dotnet/extensions/pull/5678) - Add API allowing to disable retries for a given list of HTTP methods by [@​iliar-turdushev](https://redirect.github.com/iliar-turdushev) in [https://github.com/dotnet/extensions/pull/5634](https://redirect.github.com/dotnet/extensions/pull/5634) - \[main] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5693](https://redirect.github.com/dotnet/extensions/pull/5693) - Update M.E.AI code coverage mins from 0 by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5698](https://redirect.github.com/dotnet/extensions/pull/5698) - \[main] Update dependencies from dotnet/aspnetcore by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5702](https://redirect.github.com/dotnet/extensions/pull/5702) - Improve FakeTimeProvider documentation, remove redundant tests by [@​amadeuszl](https://redirect.github.com/amadeuszl) in [https://github.com/dotnet/extensions/pull/5683](https://redirect.github.com/dotnet/extensions/pull/5683) - \[main] Update dependencies from dotnet/aspnetcore by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5703](https://redirect.github.com/dotnet/extensions/pull/5703) - For AI integration tests, use config including user secrets by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5706](https://redirect.github.com/dotnet/extensions/pull/5706) - Fix handling of text-only user messages in AzureAIInferenceChatClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5714](https://redirect.github.com/dotnet/extensions/pull/5714) - Make UseLogging a nop when NullLoggerFactory is used by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5717](https://redirect.github.com/dotnet/extensions/pull/5717) - Fix streaming function calling by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5718](https://redirect.github.com/dotnet/extensions/pull/5718) - Removes Experimental attribute from ResilienceHandler class by [@​iliar-turdushev](https://redirect.github.com/iliar-turdushev) in [https://github.com/dotnet/extensions/pull/5670](https://redirect.github.com/dotnet/extensions/pull/5670) - Usage aggregation via Dictionary\ by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5709](https://redirect.github.com/dotnet/extensions/pull/5709) - Update otel chat client / embedding generator for 1.29 draft of the spec by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5712](https://redirect.github.com/dotnet/extensions/pull/5712) - Additional tests to the logging generator by [@​rainsxng](https://redirect.github.com/rainsxng) in [https://github.com/dotnet/extensions/pull/5704](https://redirect.github.com/dotnet/extensions/pull/5704) - Update OpenAI dependency to 2.1.0 by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5725](https://redirect.github.com/dotnet/extensions/pull/5725) - Fix build by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5728](https://redirect.github.com/dotnet/extensions/pull/5728) - Add a few missing options to OpenAIChatclient.ToOpenAIOptions by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5727](https://redirect.github.com/dotnet/extensions/pull/5727) - Ollama support for streaming function calling and native structured output by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5730](https://redirect.github.com/dotnet/extensions/pull/5730) - Improve reliability of CompleteAsync_StructuredOutputEnum test by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5731](https://redirect.github.com/dotnet/extensions/pull/5731) - Add OpenAI serialization helper methods. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5697](https://redirect.github.com/dotnet/extensions/pull/5697) - Remove obsolete files by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5733](https://redirect.github.com/dotnet/extensions/pull/5733) - Update Azure.AI.OpenAI version to 2.1.0 by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5732](https://redirect.github.com/dotnet/extensions/pull/5732) - Bump code coverage by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5700](https://redirect.github.com/dotnet/extensions/pull/5700) - Update to public versions by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5749](https://redirect.github.com/dotnet/extensions/pull/5749) - HybridCache (tests only): add explicit System.Runtime.Caching dependency (CVE-related) by [@​mgravell](https://redirect.github.com/mgravell) in [https://github.com/dotnet/extensions/pull/5755](https://redirect.github.com/dotnet/extensions/pull/5755) - Disable tests not intended to run on MacOS by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5772](https://redirect.github.com/dotnet/extensions/pull/5772) - Fix hashing in Logging source gen by [@​tarekgh](https://redirect.github.com/tarekgh) in [https://github.com/dotnet/extensions/pull/5776](https://redirect.github.com/dotnet/extensions/pull/5776) - Bump update-dotnet-sdk action by [@​martincostello](https://redirect.github.com/martincostello) in [https://github.com/dotnet/extensions/pull/5768](https://redirect.github.com/dotnet/extensions/pull/5768) - \[main] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5778](https://redirect.github.com/dotnet/extensions/pull/5778) - Add a grouping Activity to FunctionInvokingChatClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5777](https://redirect.github.com/dotnet/extensions/pull/5777) - Adjust hashing in logging source gen by [@​tarekgh](https://redirect.github.com/tarekgh) in [https://github.com/dotnet/extensions/pull/5782](https://redirect.github.com/dotnet/extensions/pull/5782) - Respect AutoAdvance for GetTimestamp in FakeTimeProvider by [@​amadeuszl](https://redirect.github.com/amadeuszl) in [https://github.com/dotnet/extensions/pull/5783](https://redirect.github.com/dotnet/extensions/pull/5783) #### New Contributors - [@​haipz](https://redirect.github.com/haipz) made their first contribution in [https://github.com/dotnet/extensions/pull/5597](https://redirect.github.com/dotnet/extensions/pull/5597) - [@​amadeuszl](https://redirect.github.com/amadeuszl) made their first contribution in [https://github.com/dotnet/extensions/pull/5646](https://redirect.github.com/dotnet/extensions/pull/5646) - [@​rainsxng](https://redirect.github.com/rainsxng) made their first contribution in [https://github.com/dotnet/extensions/pull/5704](https://redirect.github.com/dotnet/extensions/pull/5704) - [@​tarekgh](https://redirect.github.com/tarekgh) made their first contribution in [https://github.com/dotnet/extensions/pull/5776](https://redirect.github.com/dotnet/extensions/pull/5776) **Full Changelog**: https://github.com/dotnet/extensions/compare/v9.0.0...v9.1.0
dotnet/sdk (dotnet-sdk) ### [`v9.0.102`](https://redirect.github.com/dotnet/sdk/compare/v9.0.101-servicing.24572.9...v9.0.102) [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v9.0.101-servicing.24572.9...v9.0.102)
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] 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> --- Directory.Packages.props | 20 ++++++++++---------- global.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 38878dcd..b2d22423 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,14 +5,14 @@
- - - - - - - - + + + + + + + + @@ -23,7 +23,7 @@ - + @@ -31,7 +31,7 @@ - + diff --git a/global.json b/global.json index 5ead15e7..79ead71d 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMajor", - "version": "9.0.101", + "version": "9.0.102", "allowPrerelease": false } } From e3965dbc31561c9a09342f8808f1175974e14317 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 4 Feb 2025 09:37:57 -0500 Subject: [PATCH 240/316] chore: update renovate config to extend the shared config (#364) ## This PR - update renovate config to extend the shared config ### Notes This will use the shared confirm managed in the community tooling repo. The noteworthy changes are are merging for minor versions that are sub-v1. https://github.com/open-feature/community-tooling/blob/main/renovate.json Signed-off-by: Michael Beemer --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 39a2b6e9..daaea0b5 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "github>open-feature/community-tooling" ] } From 0cb58db59573f9f8266fc417083c91e86e499772 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:14:50 -0500 Subject: [PATCH 241/316] chore(deps): update spec digest to 8d6eeb3 (#366) 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 | `b58c3b4` -> `8d6eeb3` | --- ### 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 b58c3b4e..8d6eeb32 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit b58c3b4ec68b0db73e6c33ed4a30e94b1ede5e85 +Subproject commit 8d6eeb3247600f6f66ffc92afa50ebde75b4d3ce From fb8e5aa9d3a020de3ea57948130951d0d282f465 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:15:13 -0500 Subject: [PATCH 242/316] chore(deps): update dependency xunit to 2.9.3 (#340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [xunit](https://redirect.github.com/xunit/xunit) | `2.9.2` -> `2.9.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit/2.9.2/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit/2.9.2/2.9.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b2d22423..f12cc223 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + From 3160cd2262739ba0a2981a9dc04fc0d278799546 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:26:31 -0500 Subject: [PATCH 243/316] chore(deps): pin dependencies (#365) 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 | |---|---|---|---| | [actions/checkout](https://redirect.github.com/actions/checkout) | action | pinDigest | -> `11bd719` | | [actions/setup-dotnet](https://redirect.github.com/actions/setup-dotnet) | action | pinDigest | -> `3951f0d` | | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | pinDigest | -> `6f51ac0` | | [amannn/action-semantic-pull-request](https://redirect.github.com/amannn/action-semantic-pull-request) | action | pinDigest | -> `0723387` | | [codecov/codecov-action](https://redirect.github.com/codecov/codecov-action) | action | pinDigest | -> `13ce06b` | | [github/codeql-action](https://redirect.github.com/github/codeql-action) | action | pinDigest | -> `dd74661` | | [google-github-actions/release-please-action](https://redirect.github.com/google-github-actions/release-please-action) | action | pinDigest | -> `db8f2c6` | --- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] 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/ci.yml | 10 +++++----- .github/workflows/code-coverage.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 8 ++++---- .github/workflows/dotnet-format.yml | 4 ++-- .github/workflows/e2e.yml | 4 ++-- .github/workflows/lint-pr.yml | 2 +- .github/workflows/release.yml | 8 ++++---- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd3e4d23..7fcdafe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,13 +20,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -55,13 +55,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.5.0 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: nupkgs path: src/**/*.nupkg diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index ff5e77ab..988e9051 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -19,12 +19,12 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -36,7 +36,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v5.3.1 + - uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6968bd71..ad0b1db7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # 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@v3 + uses: github/codeql-action/autobuild@dd746615b3b9d728a6a37ca2045b68ca76d4841a # 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@v3 + uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3 diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index a6d5e36e..abf45a70 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -12,10 +12,10 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 with: dotnet-version: 9.0.x diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 80c538e0..b1458b8b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -14,12 +14,12 @@ jobs: e2e-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 279e8f55..5dbb5688 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -12,6 +12,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5f38efa..66962033 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v3 + - uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 id: release with: command: manifest @@ -27,12 +27,12 @@ jobs: if: ${{ needs.release-please.outputs.release_created }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -57,7 +57,7 @@ jobs: if: ${{ needs.release-please.outputs.release_created }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 From 9b0b3195fa206f11c1acc7336c4e4f6252b8b2ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:26:57 -0500 Subject: [PATCH 244/316] chore(deps): update dependency autofixture to 5.0.0-preview0012 (#351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [AutoFixture](https://redirect.github.com/AutoFixture/AutoFixture) | `5.0.0-preview0011` -> `5.0.0-preview0012` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/AutoFixture/5.0.0-preview0012?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/AutoFixture/5.0.0-preview0012?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/AutoFixture/5.0.0-preview0011/5.0.0-preview0012?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/AutoFixture/5.0.0-preview0011/5.0.0-preview0012?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f12cc223..5f82a584 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ - + From 5ebe4f685ec0bf4d7d0acbf3790c908e04c5efd7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:27:13 -0500 Subject: [PATCH 245/316] chore(deps): update dependency coverlet.msbuild to 6.0.4 (#348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [coverlet.msbuild](https://redirect.github.com/coverlet-coverage/coverlet) | `6.0.3` -> `6.0.4` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/coverlet.msbuild/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/coverlet.msbuild/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/coverlet.msbuild/6.0.3/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/coverlet.msbuild/6.0.3/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
coverlet-coverage/coverlet (coverlet.msbuild) ### [`v6.0.4`](https://redirect.github.com/coverlet-coverage/coverlet/releases/tag/v6.0.4) ##### Fixed - Fix empty coverage report when using include and exclude filters [#​1726](https://redirect.github.com/coverlet-coverage/coverlet/issues/1726) [Diff between 6.0.3 and 6.0.4](https://redirect.github.com/coverlet-coverage/coverlet/compare/v6.0.3...v6.0.4)
--- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5f82a584..ace9f3d5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + From e59034dd56038351f79a7e226adae268d172ccbb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:39:20 +0000 Subject: [PATCH 246/316] chore(deps): update dependency coverlet.collector to 6.0.4 (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [coverlet.collector](https://redirect.github.com/coverlet-coverage/coverlet) | `6.0.3` -> `6.0.4` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/coverlet.collector/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/coverlet.collector/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/coverlet.collector/6.0.3/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/coverlet.collector/6.0.3/6.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
coverlet-coverage/coverlet (coverlet.collector) ### [`v6.0.4`](https://redirect.github.com/coverlet-coverage/coverlet/releases/tag/v6.0.4) ##### Fixed - Fix empty coverage report when using include and exclude filters [#​1726](https://redirect.github.com/coverlet-coverage/coverlet/issues/1726) [Diff between 6.0.3 and 6.0.4](https://redirect.github.com/coverlet-coverage/coverlet/compare/v6.0.3...v6.0.4)
--- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ace9f3d5..92a172f0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From cb7105b21c4e0fc1674365f9fd6a4b26e95f45c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:39:31 +0000 Subject: [PATCH 247/316] chore(deps): update actions/upload-artifact action to v4.6.0 (#341) 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 | |---|---|---|---| | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | minor | `v4.5.0` -> `v4.6.0` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.6.0`](https://redirect.github.com/actions/upload-artifact/releases/tag/v4.6.0) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.5.0...v4.6.0) #### What's Changed - Expose env vars to control concurrency and timeout by [@​yacaovsnc](https://redirect.github.com/yacaovsnc) in [https://github.com/actions/upload-artifact/pull/662](https://redirect.github.com/actions/upload-artifact/pull/662) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4...v4.6.0
--- ### 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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fcdafe9..0967b0f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: nupkgs path: src/**/*.nupkg From dad62826404e1d2e679ef35560a1dd858c95ffdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:05:48 +0000 Subject: [PATCH 248/316] fix: Fix SBOM release pipeline (#367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR - This pull request includes an update to the `.github/workflows/release.yml` file to set up the .NET SDK for the release workflow. ### Related Issues Fixes #362 ### Notes * Added a step to set up the .NET SDK using the `actions/setup-dotnet` action, specifying the .NET version `9.0.x` and the source URL for the NuGet package. ### How to test Check green build here: https://github.com/open-feature/dotnet-sdk/actions/runs/13141307066/job/36668893117 Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/release.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66962033..20f4b9ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,6 +61,15 @@ jobs: with: fetch-depth: 0 + - name: Setup .NET SDK + uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: | + 9.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json + - name: Install CycloneDX.NET run: dotnet tool install CycloneDX From 24a7767f0e9df73734693027c10729a9f3d8d596 Mon Sep 17 00:00:00 2001 From: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:16:54 -0500 Subject: [PATCH 249/316] chore(main): release 2.3.1 (#363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.3.1](https://github.com/open-feature/dotnet-sdk/compare/v2.3.0...v2.3.1) (2025-02-04) ### πŸ› Bug Fixes * Fix SBOM release pipeline ([#367](https://github.com/open-feature/dotnet-sdk/issues/367)) ([dad6282](https://github.com/open-feature/dotnet-sdk/commit/dad62826404e1d2e679ef35560a1dd858c95ffdc)) ### 🧹 Chore * **deps:** pin dependencies ([#365](https://github.com/open-feature/dotnet-sdk/issues/365)) ([3160cd2](https://github.com/open-feature/dotnet-sdk/commit/3160cd2262739ba0a2981a9dc04fc0d278799546)) * **deps:** update actions/upload-artifact action to v4.6.0 ([#341](https://github.com/open-feature/dotnet-sdk/issues/341)) ([cb7105b](https://github.com/open-feature/dotnet-sdk/commit/cb7105b21c4e0fc1674365f9fd6a4b26e95f45c3)) * **deps:** update dependency autofixture to 5.0.0-preview0012 ([#351](https://github.com/open-feature/dotnet-sdk/issues/351)) ([9b0b319](https://github.com/open-feature/dotnet-sdk/commit/9b0b3195fa206f11c1acc7336c4e4f6252b8b2ad)) * **deps:** update dependency coverlet.collector to 6.0.4 ([#347](https://github.com/open-feature/dotnet-sdk/issues/347)) ([e59034d](https://github.com/open-feature/dotnet-sdk/commit/e59034dd56038351f79a7e226adae268d172ccbb)) * **deps:** update dependency coverlet.msbuild to 6.0.4 ([#348](https://github.com/open-feature/dotnet-sdk/issues/348)) ([5ebe4f6](https://github.com/open-feature/dotnet-sdk/commit/5ebe4f685ec0bf4d7d0acbf3790c908e04c5efd7)) * **deps:** update dependency xunit to 2.9.3 ([#340](https://github.com/open-feature/dotnet-sdk/issues/340)) ([fb8e5aa](https://github.com/open-feature/dotnet-sdk/commit/fb8e5aa9d3a020de3ea57948130951d0d282f465)) * **deps:** update dotnet monorepo ([#343](https://github.com/open-feature/dotnet-sdk/issues/343)) ([32dab9b](https://github.com/open-feature/dotnet-sdk/commit/32dab9ba0904a6b27fc53e21402ae95d5594ad01)) * **deps:** update spec digest to 8d6eeb3 ([#366](https://github.com/open-feature/dotnet-sdk/issues/366)) ([0cb58db](https://github.com/open-feature/dotnet-sdk/commit/0cb58db59573f9f8266fc417083c91e86e499772)) * update renovate config to extend the shared config ([#364](https://github.com/open-feature/dotnet-sdk/issues/364)) ([e3965db](https://github.com/open-feature/dotnet-sdk/commit/e3965dbc31561c9a09342f8808f1175974e14317)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9965a341..aca3a494 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.0" + ".": "2.3.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f07a6552..6a9513b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [2.3.1](https://github.com/open-feature/dotnet-sdk/compare/v2.3.0...v2.3.1) (2025-02-04) + + +### πŸ› Bug Fixes + +* Fix SBOM release pipeline ([#367](https://github.com/open-feature/dotnet-sdk/issues/367)) ([dad6282](https://github.com/open-feature/dotnet-sdk/commit/dad62826404e1d2e679ef35560a1dd858c95ffdc)) + + +### 🧹 Chore + +* **deps:** pin dependencies ([#365](https://github.com/open-feature/dotnet-sdk/issues/365)) ([3160cd2](https://github.com/open-feature/dotnet-sdk/commit/3160cd2262739ba0a2981a9dc04fc0d278799546)) +* **deps:** update actions/upload-artifact action to v4.6.0 ([#341](https://github.com/open-feature/dotnet-sdk/issues/341)) ([cb7105b](https://github.com/open-feature/dotnet-sdk/commit/cb7105b21c4e0fc1674365f9fd6a4b26e95f45c3)) +* **deps:** update dependency autofixture to 5.0.0-preview0012 ([#351](https://github.com/open-feature/dotnet-sdk/issues/351)) ([9b0b319](https://github.com/open-feature/dotnet-sdk/commit/9b0b3195fa206f11c1acc7336c4e4f6252b8b2ad)) +* **deps:** update dependency coverlet.collector to 6.0.4 ([#347](https://github.com/open-feature/dotnet-sdk/issues/347)) ([e59034d](https://github.com/open-feature/dotnet-sdk/commit/e59034dd56038351f79a7e226adae268d172ccbb)) +* **deps:** update dependency coverlet.msbuild to 6.0.4 ([#348](https://github.com/open-feature/dotnet-sdk/issues/348)) ([5ebe4f6](https://github.com/open-feature/dotnet-sdk/commit/5ebe4f685ec0bf4d7d0acbf3790c908e04c5efd7)) +* **deps:** update dependency xunit to 2.9.3 ([#340](https://github.com/open-feature/dotnet-sdk/issues/340)) ([fb8e5aa](https://github.com/open-feature/dotnet-sdk/commit/fb8e5aa9d3a020de3ea57948130951d0d282f465)) +* **deps:** update dotnet monorepo ([#343](https://github.com/open-feature/dotnet-sdk/issues/343)) ([32dab9b](https://github.com/open-feature/dotnet-sdk/commit/32dab9ba0904a6b27fc53e21402ae95d5594ad01)) +* **deps:** update spec digest to 8d6eeb3 ([#366](https://github.com/open-feature/dotnet-sdk/issues/366)) ([0cb58db](https://github.com/open-feature/dotnet-sdk/commit/0cb58db59573f9f8266fc417083c91e86e499772)) +* update renovate config to extend the shared config ([#364](https://github.com/open-feature/dotnet-sdk/issues/364)) ([e3965db](https://github.com/open-feature/dotnet-sdk/commit/e3965dbc31561c9a09342f8808f1175974e14317)) + ## [2.3.0](https://github.com/open-feature/dotnet-sdk/compare/v2.2.0...v2.3.0) (2025-01-31) diff --git a/README.md b/README.md index eae48181..b02f5721 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v2.3.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.0) + ![Release](https://img.shields.io/static/v1?label=release&message=v2.3.1&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.1) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index 624a7f50..db886413 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.3.0 + 2.3.1 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 276cbf9e..2bf1c1cc 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.3.0 +2.3.1 From e74e8e7a58d90e46bbcd5d7e9433545412e07bbd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:41:24 +0000 Subject: [PATCH 250/316] chore(deps): update github/codeql-action digest to 9e8d078 (#371) 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://redirect.github.com/github/codeql-action) | action | digest | `dd74661` -> `9e8d078` | --- ### 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 ad0b1db7..5c269e20 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@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3 + uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # 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@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3 + uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # 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@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3 + uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 From 4ecfd249181cf8fe372810a1fc3369347c6302fc Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:36:47 +0000 Subject: [PATCH 251/316] chore: remove FluentAssertions (#361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR - Removes FluentAssertions in unit, integration, and E2E tests - Update CONTRIBUTING.md to fix out of date documentation ### Related Issues Fixes #353 ### Notes ### Follow-up Tasks ### How to test --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> Co-authored-by: Michael Beemer Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- Directory.Packages.props | 1 - .../FeatureLifecycleManagerTests.cs | 7 +- ...enFeature.DependencyInjection.Tests.csproj | 1 - .../OpenFeatureBuilderExtensionsTests.cs | 51 ++++---- ...FeatureServiceCollectionExtensionsTests.cs | 7 +- .../OpenFeature.E2ETests.csproj | 1 - .../FeatureFlagIntegrationTest.cs | 9 +- .../OpenFeature.IntegrationTests.csproj | 1 - .../FeatureProviderExceptionTests.cs | 11 +- .../OpenFeature.Tests/FeatureProviderTests.cs | 46 ++++--- .../OpenFeature.Tests.csproj | 1 - .../OpenFeatureClientTests.cs | 112 +++++++++--------- .../OpenFeatureEvaluationContextTests.cs | 35 +++--- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 27 ++--- test/OpenFeature.Tests/OpenFeatureTests.cs | 47 ++++---- 16 files changed, 170 insertions(+), 189 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdac14e2..48fe03bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ dotnet test test/OpenFeature.Tests/ To be able to run the e2e tests, first we need to initialize the submodule and copy the test files: ```bash -git submodule update --init --recursive && cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ +git submodule update --init --recursive && cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ ``` Now you can run the tests using: diff --git a/Directory.Packages.props b/Directory.Packages.props index 92a172f0..adcf05a6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,6 @@ - diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index b0176bc4..c5573604 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -44,7 +43,7 @@ public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExi await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(true); // Assert - Api.Instance.GetProvider().Should().BeSameAs(featureProvider); + Assert.Equal(featureProvider, Api.Instance.GetProvider()); } [Fact] @@ -58,7 +57,7 @@ public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNo // Assert var exception = await Assert.ThrowsAsync(act).ConfigureAwait(true); - exception.Should().NotBeNull(); - exception.Message.Should().NotBeNullOrWhiteSpace(); + Assert.NotNull(exception); + Assert.False(string.IsNullOrWhiteSpace(exception.Message)); } } diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj index e4c16ee5..4d714afe 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -20,7 +20,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 087336a0..6985125d 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenFeature.Model; @@ -28,12 +27,11 @@ public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProv _systemUnderTest.AddContext((_, _) => { }); // Assert - featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); - _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); - _services.Should().ContainSingle(serviceDescriptor => + Assert.Equal(_systemUnderTest, featureBuilder); + Assert.True(_systemUnderTest.IsContextConfigured, "The context should be configured."); + Assert.Single(_services, serviceDescriptor => serviceDescriptor.ServiceType == typeof(EvaluationContext) && - serviceDescriptor.Lifetime == ServiceLifetime.Transient, - "A transient service of type EvaluationContext should be added."); + serviceDescriptor.Lifetime == ServiceLifetime.Transient); } [Theory] @@ -54,9 +52,9 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe var context = serviceProvider.GetService(); // Assert - _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); - context.Should().NotBeNull("The EvaluationContext should be resolvable."); - delegateCalled.Should().BeTrue("The delegate should be invoked."); + Assert.True(_systemUnderTest.IsContextConfigured, "The context should be configured."); + Assert.NotNull(context); + Assert.True(delegateCalled, "The delegate should be invoked."); } #if NET8_0_OR_GREATER @@ -80,15 +78,14 @@ public void AddProvider_ShouldAddProviderToCollection(int providerRegistrationTy }; // Assert - _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); - _systemUnderTest.HasDefaultProvider.Should().Be(expectsDefaultProvider, "The default provider flag should be set correctly."); - _systemUnderTest.IsPolicyConfigured.Should().BeFalse("The policy should not be configured."); - _systemUnderTest.DomainBoundProviderRegistrationCount.Should().Be(expectsDomainBoundProvider, "The domain-bound provider count should be correct."); - featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); - _services.Should().ContainSingle(serviceDescriptor => + Assert.False(_systemUnderTest.IsContextConfigured, "The context should not be configured."); + Assert.Equal(expectsDefaultProvider, _systemUnderTest.HasDefaultProvider); + Assert.False(_systemUnderTest.IsPolicyConfigured, "The policy should not be configured."); + Assert.Equal(expectsDomainBoundProvider, _systemUnderTest.DomainBoundProviderRegistrationCount); + Assert.Equal(_systemUnderTest, featureBuilder); + Assert.Single(_services, serviceDescriptor => serviceDescriptor.ServiceType == typeof(FeatureProvider) && - serviceDescriptor.Lifetime == ServiceLifetime.Transient, - "A singleton service of type FeatureProvider should be added."); + serviceDescriptor.Lifetime == ServiceLifetime.Transient); } class TestOptions : OpenFeatureOptions { } @@ -124,8 +121,8 @@ public void AddProvider_ShouldResolveCorrectProvider(int providerRegistrationTyp }; // Assert - provider.Should().NotBeNull("The FeatureProvider should be resolvable."); - provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + Assert.NotNull(provider); + Assert.IsType(provider); } [Theory] @@ -172,11 +169,11 @@ public void AddProvider_VerifiesDefaultAndDomainBoundProvidersBasedOnConfigurati }; // Assert - _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); - _systemUnderTest.HasDefaultProvider.Should().Be(expectsDefaultProvider, "The default provider flag should be set correctly."); - _systemUnderTest.IsPolicyConfigured.Should().BeFalse("The policy should not be configured."); - _systemUnderTest.DomainBoundProviderRegistrationCount.Should().Be(expectsDomainBoundProvider, "The domain-bound provider count should be correct."); - featureBuilder.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + Assert.False(_systemUnderTest.IsContextConfigured, "The context should not be configured."); + Assert.Equal(expectsDefaultProvider, _systemUnderTest.HasDefaultProvider); + Assert.False(_systemUnderTest.IsPolicyConfigured, "The policy should not be configured."); + Assert.Equal(expectsDomainBoundProvider, _systemUnderTest.DomainBoundProviderRegistrationCount); + Assert.Equal(_systemUnderTest, featureBuilder); } [Theory] @@ -240,8 +237,8 @@ public void AddProvider_ConfiguresPolicyNameAcrossMultipleProviderSetups(int pro serviceProvider.GetRequiredKeyedService(name); // Assert - featureBuilder.IsPolicyConfigured.Should().BeTrue("The policy should be configured."); - provider.Should().NotBeNull("The FeatureProvider should be resolvable."); - provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + Assert.True(featureBuilder.IsPolicyConfigured, "The policy should be configured."); + Assert.NotNull(provider); + Assert.IsType(provider); } } diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs index 40e761d2..d3ce5c8e 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using Xunit; @@ -22,9 +21,9 @@ public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSinglet // Act _systemUnderTest.AddOpenFeature(_configureAction); - _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); - _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); - _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); + Assert.Single(_systemUnderTest, s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); + Assert.Single(_systemUnderTest, s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); + Assert.Single(_systemUnderTest, s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); } [Fact] diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 50cf1a6a..001a3a36 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -19,7 +19,6 @@ - diff --git a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs index 559bf4bb..24561f1a 100644 --- a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs +++ b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs @@ -1,6 +1,5 @@ using System.Net.Http.Json; using System.Text.Json; -using FluentAssertions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; @@ -54,10 +53,10 @@ public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string us var responseContent = await response.Content.ReadFromJsonAsync>().ConfigureAwait(true); ; // Assert - response.IsSuccessStatusCode.Should().BeTrue("Expected HTTP status code 200 OK."); - responseContent.Should().NotBeNull("Expected response content to be non-null."); - responseContent!.FeatureName.Should().Be(FeatureA, "Expected feature name to be 'feature-a'."); - responseContent.FeatureValue.Should().Be(expectedResult, "Expected feature value to match the expected result."); + Assert.True(response.IsSuccessStatusCode, "Expected HTTP status code 200 OK."); + Assert.NotNull(responseContent); + Assert.Equal(FeatureA, responseContent!.FeatureName); + Assert.Equal(expectedResult, responseContent.FeatureValue); } private static async Task CreateServerAsync(Action? configureServices = null) diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj index 13c2f21e..e95aa810 100644 --- a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj +++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj @@ -15,7 +15,6 @@ -
diff --git a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs index fe011711..1498056f 100644 --- a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs @@ -1,5 +1,4 @@ using System; -using FluentAssertions; using OpenFeature.Constant; using OpenFeature.Error; using OpenFeature.Extension; @@ -18,7 +17,8 @@ public class FeatureProviderExceptionTests public void FeatureProviderException_Should_Resolve_Description(ErrorType errorType, string errorDescription) { var ex = new FeatureProviderException(errorType); - ex.ErrorType.GetDescription().Should().Be(errorDescription); + + Assert.Equal(errorDescription, ex.ErrorType.GetDescription()); } [Theory] @@ -27,9 +27,10 @@ public void FeatureProviderException_Should_Resolve_Description(ErrorType errorT public void FeatureProviderException_Should_Allow_Custom_ErrorCode_Messages(ErrorType errorCode, string message) { var ex = new FeatureProviderException(errorCode, message, new ArgumentOutOfRangeException("flag")); - ex.ErrorType.Should().Be(errorCode); - ex.Message.Should().Be(message); - ex.InnerException.Should().BeOfType(); + + Assert.Equal(errorCode, ex.ErrorType); + Assert.Equal(message, ex.Message); + Assert.IsType(ex.InnerException); } private enum TestEnum diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index 53a67443..ce1de36b 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using AutoFixture; -using FluentAssertions; using NSubstitute; using OpenFeature.Constant; using OpenFeature.Model; @@ -19,7 +18,7 @@ public void Provider_Must_Have_Metadata() { var provider = new TestProvider(); - provider.GetMetadata().Name.Should().Be(TestProvider.DefaultName); + Assert.Equal(TestProvider.DefaultName, provider.GetMetadata().Name); } [Fact] @@ -44,28 +43,23 @@ public async Task Provider_Must_Resolve_Flag_Values() var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveBooleanValueAsync(flagName, defaultBoolValue)).Should() - .BeEquivalentTo(boolResolutionDetails); + Assert.Equivalent(boolResolutionDetails, await provider.ResolveBooleanValueAsync(flagName, defaultBoolValue)); var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveIntegerValueAsync(flagName, defaultIntegerValue)).Should() - .BeEquivalentTo(integerResolutionDetails); + Assert.Equivalent(integerResolutionDetails, await provider.ResolveIntegerValueAsync(flagName, defaultIntegerValue)); var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveDoubleValueAsync(flagName, defaultDoubleValue)).Should() - .BeEquivalentTo(doubleResolutionDetails); + Assert.Equivalent(doubleResolutionDetails, await provider.ResolveDoubleValueAsync(flagName, defaultDoubleValue)); var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStringValueAsync(flagName, defaultStringValue)).Should() - .BeEquivalentTo(stringResolutionDetails); + Assert.Equivalent(stringResolutionDetails, await provider.ResolveStringValueAsync(flagName, defaultStringValue)); var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await provider.ResolveStructureValueAsync(flagName, defaultStructureValue)).Should() - .BeEquivalentTo(structureResolutionDetails); + Assert.Equivalent(structureResolutionDetails, await provider.ResolveStructureValueAsync(flagName, defaultStructureValue)); } [Fact] @@ -113,32 +107,32 @@ public async Task Provider_Must_ErrorType() NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); var boolRes = await providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue); - boolRes.ErrorType.Should().Be(ErrorType.General); - boolRes.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.General, boolRes.ErrorType); + Assert.Equal(testMessage, boolRes.ErrorMessage); var intRes = await providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue); - intRes.ErrorType.Should().Be(ErrorType.ParseError); - intRes.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.ParseError, intRes.ErrorType); + Assert.Equal(testMessage, intRes.ErrorMessage); var doubleRes = await providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue); - doubleRes.ErrorType.Should().Be(ErrorType.InvalidContext); - doubleRes.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.InvalidContext, doubleRes.ErrorType); + Assert.Equal(testMessage, doubleRes.ErrorMessage); var stringRes = await providerMock.ResolveStringValueAsync(flagName, defaultStringValue); - stringRes.ErrorType.Should().Be(ErrorType.TypeMismatch); - stringRes.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.TypeMismatch, stringRes.ErrorType); + Assert.Equal(testMessage, stringRes.ErrorMessage); var structRes1 = await providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue); - structRes1.ErrorType.Should().Be(ErrorType.FlagNotFound); - structRes1.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.FlagNotFound, structRes1.ErrorType); + Assert.Equal(testMessage, structRes1.ErrorMessage); var structRes2 = await providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue); - structRes2.ErrorType.Should().Be(ErrorType.ProviderNotReady); - structRes2.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.ProviderNotReady, structRes2.ErrorType); + Assert.Equal(testMessage, structRes2.ErrorMessage); var boolRes2 = await providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue); - boolRes2.ErrorType.Should().Be(ErrorType.TargetingKeyMissing); - boolRes2.ErrorMessage.Should().BeNull(); + Assert.Equal(ErrorType.TargetingKeyMissing, boolRes2.ErrorType); + Assert.Null(boolRes2.ErrorMessage); } } } diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 270b6a50..a556655a 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -16,7 +16,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 1cab2d76..c16824cb 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using AutoFixture; -using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.ExceptionExtensions; @@ -37,12 +36,13 @@ public void OpenFeatureClient_Should_Allow_Hooks() client.AddHooks(new[] { hook1, hook2 }); - client.GetHooks().Should().ContainInOrder(hook1, hook2); - client.GetHooks().Count().Should().Be(2); + var expectedHooks = new[] { hook1, hook2 }.AsEnumerable(); + Assert.Equal(expectedHooks, client.GetHooks()); client.AddHooks(hook3); - client.GetHooks().Should().ContainInOrder(hook1, hook2, hook3); - client.GetHooks().Count().Should().Be(3); + + expectedHooks = new[] { hook1, hook2, hook3 }.AsEnumerable(); + Assert.Equal(expectedHooks, client.GetHooks()); client.ClearHooks(); Assert.Empty(client.GetHooks()); @@ -57,8 +57,8 @@ public void OpenFeatureClient_Metadata_Should_Have_Name() var clientVersion = fixture.Create(); var client = Api.Instance.GetClient(domain, clientVersion); - client.GetMetadata().Name.Should().Be(domain); - client.GetMetadata().Version.Should().Be(clientVersion); + Assert.Equal(domain, client.GetMetadata().Name); + Assert.Equal(clientVersion, client.GetMetadata().Version); } [Fact] @@ -81,25 +81,25 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); var client = Api.Instance.GetClient(domain, clientVersion); - (await client.GetBooleanValueAsync(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); - (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); - (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultBoolValue); + Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue)); + Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)); + Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)); - (await client.GetIntegerValueAsync(flagName, defaultIntegerValue)).Should().Be(defaultIntegerValue); - (await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().Be(defaultIntegerValue); - (await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultIntegerValue); + Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue)); + Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)); + Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)); - (await client.GetDoubleValueAsync(flagName, defaultDoubleValue)).Should().Be(defaultDoubleValue); - (await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().Be(defaultDoubleValue); - (await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultDoubleValue); + Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue)); + Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)); + Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)); - (await client.GetStringValueAsync(flagName, defaultStringValue)).Should().Be(defaultStringValue); - (await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty)).Should().Be(defaultStringValue); - (await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().Be(defaultStringValue); + Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue)); + Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty)); + Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)); - (await client.GetObjectValueAsync(flagName, defaultStructureValue)).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(defaultStructureValue); - (await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(defaultStructureValue); + Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue)); + Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty)); + Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)); } [Fact] @@ -128,29 +128,29 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var client = Api.Instance.GetClient(domain, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().BeEquivalentTo(boolFlagEvaluationDetails); - (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(boolFlagEvaluationDetails); + Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)); + Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty)); + Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)); var integerFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue)).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)).Should().BeEquivalentTo(integerFlagEvaluationDetails); - (await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(integerFlagEvaluationDetails); + Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue)); + Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)); + Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)); var doubleFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); - (await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(doubleFlagEvaluationDetails); + Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue)); + Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)); + Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)); var stringFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetStringDetailsAsync(flagName, defaultStringValue)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty)).Should().BeEquivalentTo(stringFlagEvaluationDetails); - (await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(stringFlagEvaluationDetails); + Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue)); + Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty)); + Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)); var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - (await client.GetObjectDetailsAsync(flagName, defaultStructureValue)).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty)).Should().BeEquivalentTo(structureFlagEvaluationDetails); - (await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)).Should().BeEquivalentTo(structureFlagEvaluationDetails); + Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue)); + Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty)); + Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)); } [Fact] @@ -179,8 +179,8 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc var client = Api.Instance.GetClient(domain, clientVersion, mockedLogger); var evaluationDetails = await client.GetObjectDetailsAsync(flagName, defaultValue); - evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); - evaluationDetails.ErrorMessage.Should().Be(new InvalidCastException().Message); + Assert.Equal(ErrorType.TypeMismatch, evaluationDetails.ErrorType); + Assert.Equal(new InvalidCastException().Message, evaluationDetails.ErrorMessage); _ = mockedFeatureProvider.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); @@ -290,7 +290,7 @@ public async Task Should_Resolve_BooleanValue() await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(domain, clientVersion); - (await client.GetBooleanValueAsync(flagName, defaultValue)).Should().Be(defaultValue); + Assert.Equal(defaultValue, await client.GetBooleanValueAsync(flagName, defaultValue)); _ = featureProviderMock.Received(1).ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()); } @@ -312,7 +312,7 @@ public async Task Should_Resolve_StringValue() await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(domain, clientVersion); - (await client.GetStringValueAsync(flagName, defaultValue)).Should().Be(defaultValue); + Assert.Equal(defaultValue, await client.GetStringValueAsync(flagName, defaultValue)); _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultValue, Arg.Any()); } @@ -334,7 +334,7 @@ public async Task Should_Resolve_IntegerValue() await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(domain, clientVersion); - (await client.GetIntegerValueAsync(flagName, defaultValue)).Should().Be(defaultValue); + Assert.Equal(defaultValue, await client.GetIntegerValueAsync(flagName, defaultValue)); _ = featureProviderMock.Received(1).ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()); } @@ -356,7 +356,7 @@ public async Task Should_Resolve_DoubleValue() await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(domain, clientVersion); - (await client.GetDoubleValueAsync(flagName, defaultValue)).Should().Be(defaultValue); + Assert.Equal(defaultValue, await client.GetDoubleValueAsync(flagName, defaultValue)); _ = featureProviderMock.Received(1).ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()); } @@ -378,7 +378,7 @@ public async Task Should_Resolve_StructureValue() await Api.Instance.SetProviderAsync(featureProviderMock); var client = Api.Instance.GetClient(domain, clientVersion); - (await client.GetObjectValueAsync(flagName, defaultValue)).Should().Be(defaultValue); + Assert.Equal(defaultValue, await client.GetObjectValueAsync(flagName, defaultValue)); _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } @@ -402,9 +402,9 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() var client = Api.Instance.GetClient(domain, clientVersion); var response = await client.GetObjectDetailsAsync(flagName, defaultValue); - response.ErrorType.Should().Be(ErrorType.ParseError); - response.Reason.Should().Be(Reason.Error); - response.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.ParseError, response.ErrorType); + Assert.Equal(Reason.Error, response.Reason); + Assert.Equal(testMessage, response.ErrorMessage); _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } @@ -427,9 +427,9 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() var client = Api.Instance.GetClient(domain, clientVersion); var response = await client.GetObjectDetailsAsync(flagName, defaultValue); - response.ErrorType.Should().Be(ErrorType.ParseError); - response.Reason.Should().Be(Reason.Error); - response.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.ParseError, response.ErrorType); + Assert.Equal(Reason.Error, response.Reason); + Assert.Equal(testMessage, response.ErrorMessage); _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); } @@ -456,9 +456,9 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Not_Run_After_Hook client.AddHooks(testHook); var response = await client.GetObjectDetailsAsync(flagName, defaultValue); - response.ErrorType.Should().Be(ErrorType.ParseError); - response.Reason.Should().Be(Reason.Error); - response.ErrorMessage.Should().Be(testMessage); + Assert.Equal(ErrorType.ParseError, response.ErrorType); + Assert.Equal(Reason.Error, response.Reason); + Assert.Equal(testMessage, response.ErrorMessage); _ = featureProviderMock.Received(1) .ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); @@ -501,8 +501,8 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() cts.Cancel(); // cancel before awaiting var response = await task; - response.Value.Should().Be(defaultString); - response.Reason.Should().Be(cancelledReason); + Assert.Equal(defaultString, response.Value); + Assert.Equal(cancelledReason, response.Reason); _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultString, Arg.Any(), cts.Token); } @@ -538,7 +538,7 @@ public void ToFlagEvaluationDetails_Should_Convert_All_Properties() var expected = new ResolutionDetails(flagName, boolValue, errorType, reason, variant, errorMessage, flagMetadata); var result = expected.ToFlagEvaluationDetails(); - result.Should().BeEquivalentTo(expected); + Assert.Equivalent(expected, result); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 826ac68e..20b0ec2e 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using AutoFixture; -using FluentAssertions; using OpenFeature.Model; using OpenFeature.Tests.Internal; using Xunit; @@ -91,34 +90,34 @@ public void EvaluationContext_Should_All_Types() var context = contextBuilder.Build(); - context.TargetingKey.Should().Be("targeting_key"); + Assert.Equal("targeting_key", context.TargetingKey); var targetingKeyValue = context.GetValue(context.TargetingKey!); - targetingKeyValue.IsString.Should().BeTrue(); - targetingKeyValue.AsString.Should().Be("userId"); + Assert.True(targetingKeyValue.IsString); + Assert.Equal("userId", targetingKeyValue.AsString); var value1 = context.GetValue("key1"); - value1.IsString.Should().BeTrue(); - value1.AsString.Should().Be("value"); + Assert.True(value1.IsString); + Assert.Equal("value", value1.AsString); var value2 = context.GetValue("key2"); - value2.IsNumber.Should().BeTrue(); - value2.AsInteger.Should().Be(1); + Assert.True(value2.IsNumber); + Assert.Equal(1, value2.AsInteger); var value3 = context.GetValue("key3"); - value3.IsBoolean.Should().Be(true); - value3.AsBoolean.Should().Be(true); + Assert.True(value3.IsBoolean); + Assert.True(value3.AsBoolean); var value4 = context.GetValue("key4"); - value4.IsDateTime.Should().BeTrue(); - value4.AsDateTime.Should().Be(now); + Assert.True(value4.IsDateTime); + Assert.Equal(now, value4.AsDateTime); var value5 = context.GetValue("key5"); - value5.IsStructure.Should().BeTrue(); - value5.AsStructure.Should().Equal(structure); + Assert.True(value5.IsStructure); + Assert.Equal(structure, value5.AsStructure); var value6 = context.GetValue("key6"); - value6.IsNumber.Should().BeTrue(); - value6.AsDouble.Should().Be(1.0); + Assert.True(value6.IsNumber); + Assert.Equal(1.0, value6.AsDouble); } [Fact] @@ -146,11 +145,11 @@ public void Should_Be_Able_To_Get_All_Values() var count = 0; foreach (var keyValue in context) { - context.GetValue(keyValue.Key).AsString.Should().Be(keyValue.Value.AsString); + Assert.Equal(keyValue.Value.AsString, context.GetValue(keyValue.Key).AsString); count++; } - context.Count.Should().Be(count); + Assert.Equal(count, context.Count); } [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 442b3491..bbb4da3f 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using AutoFixture; -using FluentAssertions; using NSubstitute; using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; @@ -122,11 +121,11 @@ public void Hook_Context_Should_Have_Properties_And_Be_Immutable() var context = new HookContext("test", testStructure, FlagValueType.Object, clientMetadata, providerMetadata, EvaluationContext.Empty); - context.ClientMetadata.Should().BeSameAs(clientMetadata); - context.ProviderMetadata.Should().BeSameAs(providerMetadata); - context.FlagKey.Should().Be("test"); - context.DefaultValue.Should().BeSameAs(testStructure); - context.FlagValueType.Should().Be(FlagValueType.Object); + Assert.Equal(clientMetadata, context.ClientMetadata); + Assert.Equal(providerMetadata, context.ProviderMetadata); + Assert.Equal("test", context.FlagKey); + Assert.Equal(testStructure, context.DefaultValue); + Assert.Equal(FlagValueType.Object, context.FlagValueType); } [Fact] @@ -264,9 +263,9 @@ public async Task Hook_Should_Return_No_Errors() await hook.FinallyAsync(hookContext, evaluationDetails, hookHints); await hook.ErrorAsync(hookContext, new Exception(), hookHints); - hookContext.ClientMetadata.Name.Should().BeNull(); - hookContext.ClientMetadata.Version.Should().BeNull(); - hookContext.ProviderMetadata.Name.Should().BeNull(); + Assert.Null(hookContext.ClientMetadata.Name); + Assert.Null(hookContext.ClientMetadata.Version); + Assert.Null(hookContext.ProviderMetadata.Name); } [Fact] @@ -329,8 +328,8 @@ await client.GetBooleanValueAsync("test", false, null, new FlagEvaluationOptions(hook3, ImmutableDictionary.Empty)); Assert.Single(Api.Instance.GetHooks()); - client.GetHooks().Count().Should().Be(1); - testProvider.GetProviderHooks().Count.Should().Be(1); + Assert.Single(client.GetHooks()); + Assert.Single(testProvider.GetProviderHooks()); } [Fact] @@ -356,7 +355,7 @@ public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() await Api.Instance.SetProviderAsync(featureProvider); var client = Api.Instance.GetClient(); client.AddHooks(new[] { hook1, hook2 }); - client.GetHooks().Count().Should().Be(2); + Assert.Equal(2, client.GetHooks().Count()); await client.GetBooleanValueAsync("test", false); @@ -522,7 +521,7 @@ public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null); }); - resolvedFlag.Should().BeTrue(); + Assert.True(resolvedFlag); _ = hook.Received(1).BeforeAsync(Arg.Any>(), null); _ = hook.Received(1).ErrorAsync(Arg.Any>(), exceptionToThrow, null); _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); @@ -563,7 +562,7 @@ public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() var resolvedFlag = await client.GetBooleanValueAsync("test", true, config: flagOptions); - resolvedFlag.Should().BeTrue(); + Assert.True(resolvedFlag); Received.InOrder(() => { diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 1955f82d..24caf9ad 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; -using FluentAssertions; using NSubstitute; using OpenFeature.Constant; using OpenFeature.Model; @@ -21,7 +20,7 @@ public void OpenFeature_Should_Be_Singleton() var openFeature = Api.Instance; var openFeature2 = Api.Instance; - openFeature.Should().BeSameAs(openFeature2); + Assert.Equal(openFeature2, openFeature); } [Fact] @@ -104,8 +103,8 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def var defaultClient = openFeature.GetProviderMetadata(); var domainScopedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); - defaultClient?.Name.Should().Be(NoOpProvider.NoOpProviderName); - domainScopedClient?.Name.Should().Be(TestProvider.DefaultName); + Assert.Equal(NoOpProvider.NoOpProviderName, defaultClient?.Name); + Assert.Equal(TestProvider.DefaultName, domainScopedClient?.Name); } [Fact] @@ -118,7 +117,7 @@ public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() var defaultClient = openFeature.GetProviderMetadata(); - defaultClient?.Name.Should().Be(TestProvider.DefaultName); + Assert.Equal(TestProvider.DefaultName, defaultClient?.Name); } [Fact] @@ -131,7 +130,7 @@ public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() await openFeature.SetProviderAsync(name, new TestProvider()); await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); - openFeature.GetProviderMetadata(name)?.Name.Should().Be(NoOpProvider.NoOpProviderName); + Assert.Equal(NoOpProvider.NoOpProviderName, openFeature.GetProviderMetadata(name)?.Name); } [Fact] @@ -147,7 +146,7 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instanc var clientA = openFeature.GetProvider("a"); var clientB = openFeature.GetProvider("b"); - clientA.Should().Be(clientB); + Assert.Equal(clientB, clientA); } [Fact] @@ -164,16 +163,16 @@ public void OpenFeature_Should_Add_Hooks() openFeature.AddHooks(hook1); - openFeature.GetHooks().Should().Contain(hook1); + Assert.Contains(hook1, openFeature.GetHooks()); Assert.Single(openFeature.GetHooks()); openFeature.AddHooks(hook2); - openFeature.GetHooks().Should().ContainInOrder(hook1, hook2); - openFeature.GetHooks().Count().Should().Be(2); + var expectedHooks = new[] { hook1, hook2 }.AsEnumerable(); + Assert.Equal(expectedHooks, openFeature.GetHooks()); openFeature.AddHooks(new[] { hook3, hook4 }); - openFeature.GetHooks().Should().ContainInOrder(hook1, hook2, hook3, hook4); - openFeature.GetHooks().Count().Should().Be(4); + expectedHooks = new[] { hook1, hook2, hook3, hook4 }.AsEnumerable(); + Assert.Equal(expectedHooks, openFeature.GetHooks()); openFeature.ClearHooks(); Assert.Empty(openFeature.GetHooks()); @@ -187,8 +186,8 @@ public async Task OpenFeature_Should_Get_Metadata() var openFeature = Api.Instance; var metadata = openFeature.GetProviderMetadata(); - metadata.Should().NotBeNull(); - metadata?.Name.Should().Be(NoOpProvider.NoOpProviderName); + Assert.NotNull(metadata); + Assert.Equal(NoOpProvider.NoOpProviderName, metadata?.Name); } [Theory] @@ -201,9 +200,9 @@ public void OpenFeature_Should_Create_Client(string? name = null, string? versio var openFeature = Api.Instance; var client = openFeature.GetClient(name, version); - client.Should().NotBeNull(); - client.GetMetadata().Name.Should().Be(name); - client.GetMetadata().Version.Should().Be(version); + Assert.NotNull(client); + Assert.Equal(name, client.GetMetadata().Name); + Assert.Equal(version, client.GetMetadata().Version); } [Fact] @@ -213,19 +212,19 @@ public void Should_Set_Given_Context() Api.Instance.SetContext(context); - Api.Instance.GetContext().Should().BeSameAs(context); + Assert.Equal(context, Api.Instance.GetContext()); context = EvaluationContext.Builder().Build(); Api.Instance.SetContext(context); - Api.Instance.GetContext().Should().BeSameAs(context); + Assert.Equal(context, Api.Instance.GetContext()); } [Fact] public void Should_Always_Have_Provider() { - Api.Instance.GetProvider().Should().NotBeNull(); + Assert.NotNull(Api.Instance.GetProvider()); } [Fact] @@ -239,11 +238,11 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() var client1 = openFeature.GetClient("client1"); var client2 = openFeature.GetClient("client2"); - client1.GetMetadata().Name.Should().Be("client1"); - client2.GetMetadata().Name.Should().Be("client2"); + Assert.Equal("client1", client1.GetMetadata().Name); + Assert.Equal("client2", client2.GetMetadata().Name); - (await client1.GetBooleanValueAsync("test", false)).Should().BeTrue(); - (await client2.GetBooleanValueAsync("test", false)).Should().BeFalse(); + Assert.True(await client1.GetBooleanValueAsync("test", false)); + Assert.False(await client2.GetBooleanValueAsync("test", false)); } [Fact] From 7a735f8d8b82b79b205f71716e5cf300a7fff276 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:38:15 +0000 Subject: [PATCH 252/316] chore(deps): update dependency microsoft.net.test.sdk to 17.13.0 (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Microsoft.NET.Test.Sdk](https://redirect.github.com/microsoft/vstest) | `17.12.0` -> `17.13.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.NET.Test.Sdk/17.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.NET.Test.Sdk/17.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.NET.Test.Sdk/17.12.0/17.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.NET.Test.Sdk/17.12.0/17.13.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
microsoft/vstest (Microsoft.NET.Test.Sdk) ### [`v17.13.0`](https://redirect.github.com/microsoft/vstest/releases/tag/v17.13.0) #### What's Changed - Add letter number among valid identifiers in class name by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/13868](https://redirect.github.com/microsoft/vstest/pull/13868) - Fix formatting in Runner by [@​mthalman](https://redirect.github.com/mthalman) in [https://github.com/microsoft/vstest/pull/13871](https://redirect.github.com/microsoft/vstest/pull/13871) - Downgrade xunit skip warning to info by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/10381](https://redirect.github.com/microsoft/vstest/pull/10381) - Add msdia for arm64 into nuget by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/10382](https://redirect.github.com/microsoft/vstest/pull/10382) - Enable native debugging for vstest.console by [@​ocitrev](https://redirect.github.com/ocitrev) in [https://github.com/microsoft/vstest/pull/10401](https://redirect.github.com/microsoft/vstest/pull/10401) - Fix RFCs links by [@​Youssef1313](https://redirect.github.com/Youssef1313) in [https://github.com/microsoft/vstest/pull/10424](https://redirect.github.com/microsoft/vstest/pull/10424) - Convert to auto property by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/10365](https://redirect.github.com/microsoft/vstest/pull/10365) - Update Versions.props by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/10378](https://redirect.github.com/microsoft/vstest/pull/10378) - Enable TSA by [@​jakubch1](https://redirect.github.com/jakubch1) in [https://github.com/microsoft/vstest/pull/10385](https://redirect.github.com/microsoft/vstest/pull/10385) - Arm64 dia by [@​nohwnd](https://redirect.github.com/nohwnd) in [https://github.com/microsoft/vstest/pull/10390](https://redirect.github.com/microsoft/vstest/pull/10390) - Update source-build team references by [@​MichaelSimons](https://redirect.github.com/MichaelSimons) in [https://github.com/microsoft/vstest/pull/10388](https://redirect.github.com/microsoft/vstest/pull/10388) - Exclude .signature.p7s from nupkg file count by [@​ellahathaway](https://redirect.github.com/ellahathaway) in [https://github.com/microsoft/vstest/pull/10418](https://redirect.github.com/microsoft/vstest/pull/10418) - Set NetCurrent so that it doesn't roll forward automatically by [@​ViktorHofer](https://redirect.github.com/ViktorHofer) in [https://github.com/microsoft/vstest/pull/10622](https://redirect.github.com/microsoft/vstest/pull/10622) #### New Contributors - [@​ocitrev](https://redirect.github.com/ocitrev) made their first contribution in [https://github.com/microsoft/vstest/pull/10401](https://redirect.github.com/microsoft/vstest/pull/10401) - [@​Youssef1313](https://redirect.github.com/Youssef1313) made their first contribution in [https://github.com/microsoft/vstest/pull/10424](https://redirect.github.com/microsoft/vstest/pull/10424) **Full Changelog**: https://github.com/microsoft/vstest/compare/v17.12.0...v17.13.0
--- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index adcf05a6..710cc043 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + From ed6ee2c502b16e49c91c6363ae6b3f54401a85cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:56:38 +0000 Subject: [PATCH 253/316] chore: Replace SpecFlow with Reqnroll for testing framework (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes updates to the testing framework by replacing SpecFlow with Reqnroll across multiple files. Testing framework updates: * [`Directory.Packages.props`](diffhunk://#diff-5baf5f9e448ad54ab25a091adee0da05d4d228481c9200518fcb1b53a65d4156L29-R29): Replaced SpecFlow packages with Reqnroll.xUnit package. * [`test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj`](diffhunk://#diff-ab2ad60395e1cc72b327459243ed8c5711efbd88531a3b3b813fb6c4c6019886L19-R19): Updated package references to use Reqnroll.xUnit instead of SpecFlow packages. * [`test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs`](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL7-R7): Replaced `TechTalk.SpecFlow` with `Reqnroll` in the using directives. ### Related Issues Fixes #354 Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 4 +--- test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj | 4 +--- test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 710cc043..334c640c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,9 +25,7 @@ - - - + diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 001a3a36..2d78a9ed 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -16,9 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive
- - - + diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index d0870ec3..0713370a 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -4,7 +4,7 @@ using OpenFeature.Extension; using OpenFeature.Model; using OpenFeature.Providers.Memory; -using TechTalk.SpecFlow; +using Reqnroll; using Xunit; namespace OpenFeature.E2ETests From 96ba5686c2ba31996603f464fe7e5df9efa01a92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 19:07:38 +0000 Subject: [PATCH 254/316] chore(deps): update dependency reqnroll.xunit to 2.3.0 (#378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Reqnroll.xUnit](https://www.reqnroll.net/) ([source](https://redirect.github.com/reqnroll/Reqnroll)) | `2.2.1` -> `2.3.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Reqnroll.xUnit/2.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Reqnroll.xUnit/2.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Reqnroll.xUnit/2.2.1/2.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Reqnroll.xUnit/2.2.1/2.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
reqnroll/Reqnroll (Reqnroll.xUnit) ### [`v2.3.0`](https://redirect.github.com/reqnroll/Reqnroll/blob/HEAD/CHANGELOG.md#v230---2025-02-11) #### Improvements: - Enhance BoDi error handling to provide the name of the interface being registered when that interface has already been resolved ([#​324](https://redirect.github.com/reqnroll/Reqnroll/issues/324)) - Improve code-behind feature file compilation speed ([#​336](https://redirect.github.com/reqnroll/Reqnroll/issues/336)) - Improve parameter type naming for generic types ([#​343](https://redirect.github.com/reqnroll/Reqnroll/issues/343)) - Reqnroll.Autofac: Add default registration for IReqnrollOutputHelper ([#​357](https://redirect.github.com/reqnroll/Reqnroll/issues/357)) - Reduced MsBuild log output and consistent use of \[Reqnroll] prefix ([#​381](https://redirect.github.com/reqnroll/Reqnroll/issues/381)) - Update behavior of `ObjectContainer.IsRegistered()` to check base container for registrations, to match `Resolve()` behavior ([#​367](https://redirect.github.com/reqnroll/Reqnroll/issues/367)) - Replaced custom approach for avoiding namespace collisions with .net idiomatic approach - Support loading plugin dependencies from .deps.json on .NET Framework and Visual Studio MSBuild ([#​408](https://redirect.github.com/reqnroll/Reqnroll/issues/408)) - Support for setting `ObjectContainer.DefaultConcurrentObjectResolutionTimeout` even after creation of the container ([#​435](https://redirect.github.com/reqnroll/Reqnroll/issues/435)) - Reqnroll.Microsoft.Extensions.DependencyInjection: Include `ReqnrollLogger` class to the Reqnroll MSDI plugin based on the work of [@​StefH](https://redirect.github.com/StefH) at https://github.com/StefH/Stef.Extensions.SpecFlow.Logging ([#​321](https://redirect.github.com/reqnroll/Reqnroll/issues/321)) - Reqnroll.Assist.Dynamic: The SpecFlow.Assist.Dynamic plugin by [@​marcusoftnet](https://redirect.github.com/marcusoftnet) has now been ported to Reqnroll. ([#​377](https://redirect.github.com/reqnroll/Reqnroll/issues/377)) #### Bug fixes: - Fix: MsTest: Output is written to Console.WriteLine additionally instead of using TestContext only ([#​368](https://redirect.github.com/reqnroll/Reqnroll/issues/368)) - Fix: Deprecated dependency `Specflow.Internal.Json` is used. Relpaced with `System.Text.Json`. The dependency was used for laoding `reqnroll.json`, for Visual Studio integration and for telemetry. ([#​373](https://redirect.github.com/reqnroll/Reqnroll/issues/373)) - Fix: Error with NUnit 4: "Only static OneTimeSetUp and OneTimeTearDown are allowed for InstancePerTestCase mode" ([#​379](https://redirect.github.com/reqnroll/Reqnroll/issues/379)) - Fix: Reqnroll.Autofac: FeatureContext cannot be resolved in BeforeFeature/AfterFeature hooks ([#​340](https://redirect.github.com/reqnroll/Reqnroll/issues/340)) - Fix: Attempting to set the `ConcurrentObjectResolutionTimeout` value on the `ObjectContainer` to `TimeSpan.Zero` sometimes throws an exception if running multiple tests in parallel. ([#​440](https://redirect.github.com/reqnroll/Reqnroll/issues/440)) - Fix: Project and Package references of Reqnroll.Verify are inconsistent. ([#​446](https://redirect.github.com/reqnroll/Reqnroll/issues/446)) *Contributors of this release (in alphabetical order):* [@​Antwane](https://redirect.github.com/Antwane), [@​clrudolphi](https://redirect.github.com/clrudolphi), [@​gasparnagy](https://redirect.github.com/gasparnagy), [@​obligaron](https://redirect.github.com/obligaron), [@​olegKoshmeliuk](https://redirect.github.com/olegKoshmeliuk), [@​SeanKilleen](https://redirect.github.com/SeanKilleen), [@​StefH](https://redirect.github.com/StefH)
--- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 334c640c..0ee7a9cd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + From 3bdc79bbaa8d73c4747916d307c431990397cdde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 19:07:58 +0000 Subject: [PATCH 255/316] chore(deps): update dotnet monorepo to 9.0.2 (#377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Microsoft.Bcl.AsyncInterfaces](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [Microsoft.Extensions.DependencyInjection](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.DependencyInjection/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.DependencyInjection/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.DependencyInjection/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.DependencyInjection/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [Microsoft.Extensions.DependencyInjection.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [Microsoft.Extensions.Hosting.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [Microsoft.Extensions.Logging.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [Microsoft.Extensions.Options](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Options/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Options/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Options/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Options/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [System.Collections.Immutable](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/System.Collections.Immutable/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/System.Collections.Immutable/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/System.Collections.Immutable/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/System.Collections.Immutable/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [System.Threading.Channels](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/System.Threading.Channels/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/System.Threading.Channels/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/System.Threading.Channels/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/System.Threading.Channels/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### 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 these updates 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> --- Directory.Packages.props | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0ee7a9cd..3502cc9f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,14 +5,14 @@ - - - - - - - - + + + + + + + + From 1f13258737fa051289d51cf5a064e03b0dc936c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:46:22 +0000 Subject: [PATCH 256/316] fix: Update project name in solution file (#380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes a small change to the `OpenFeature.sln` file. The change renames the project from `"."` to `".root"` to fix the current build issues. * [`OpenFeature.sln`](diffhunk://#diff-3043b3423bc4a34abd4501e344ea4f1f3c18a61e3ca97b9f914a868fa20105b6L6-R6): Renamed project from `"."` to `".root"` to improve clarity. ### Notes - Check build for details: https://github.com/open-feature/dotnet-sdk/actions/runs/13279965378/job/37076213831 Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- OpenFeature.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenFeature.sln b/OpenFeature.sln index ff4cb97e..3b5dc901 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".", ".", "{E8916D4F-B97E-42D6-8620-ED410A106F94}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{E8916D4F-B97E-42D6-8620-ED410A106F94}" ProjectSection(SolutionItems) = preProject README.md = README.md CONTRIBUTING.md = CONTRIBUTING.md From 1e8b2307369710ea0b5ae0e8a8f1f1293ea066dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 06:50:15 +0000 Subject: [PATCH 257/316] chore(deps): update spec digest to 54952f3 (#373) 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 | `8d6eeb3` -> `54952f3` | --- ### 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). --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@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 +- test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec b/spec index 8d6eeb32..54952f3b 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 8d6eeb3247600f6f66ffc92afa50ebde75b4d3ce +Subproject commit 54952f3b545a09ce966a4dbb86c9490a1ce3333b diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 0713370a..b7e60bcb 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -38,8 +38,8 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) { } - [Given(@"a provider is registered")] - public void GivenAProviderIsRegistered() + [Given("a stable provider")] + public void GivenAStableProvider() { var memProvider = new InMemoryProvider(this.e2eFlagConfig); Api.Instance.SetProviderAsync(memProvider).Wait(); From 53ced9118ffcb8cda5142dc2f80465416922030b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 06:55:50 +0000 Subject: [PATCH 258/316] chore(deps): update dotnet monorepo (#379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | Type | Update | |---|---|---|---|---|---|---|---| | [Microsoft.AspNetCore.TestHost](https://asp.net/) ([source](https://redirect.github.com/dotnet/aspnetcore)) | `9.0.1` -> `9.0.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.AspNetCore.TestHost/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.AspNetCore.TestHost/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.AspNetCore.TestHost/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.AspNetCore.TestHost/9.0.1/9.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.Diagnostics.Testing](https://dot.net/) ([source](https://redirect.github.com/dotnet/extensions)) | `9.1.0` -> `9.2.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Diagnostics.Testing/9.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Diagnostics.Testing/9.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Diagnostics.Testing/9.1.0/9.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Diagnostics.Testing/9.1.0/9.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | minor | | [dotnet-sdk](https://redirect.github.com/dotnet/sdk) | `9.0.102` -> `9.0.200` | [![age](https://developer.mend.io/api/mc/badges/age/dotnet-version/dotnet-sdk/9.0.200?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/dotnet-version/dotnet-sdk/9.0.200?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/dotnet-version/dotnet-sdk/9.0.102/9.0.200?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/dotnet-version/dotnet-sdk/9.0.102/9.0.200?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dotnet-sdk | patch | --- ### Release Notes
dotnet/aspnetcore (Microsoft.AspNetCore.TestHost) ### [`v9.0.2`](https://redirect.github.com/dotnet/aspnetcore/compare/v9.0.1...v9.0.2)
dotnet/extensions (Microsoft.Extensions.Diagnostics.Testing) ### [`v9.2.0`](https://redirect.github.com/dotnet/extensions/releases/tag/v9.2.0) #### What's Changed - Add `FunctionInvokingChatClient.CurrentContext` by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5786](https://redirect.github.com/dotnet/extensions/pull/5786) - Fix schema generation for floating point types by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5788](https://redirect.github.com/dotnet/extensions/pull/5788) - Add an extension method for registering custom AIContent types by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5789](https://redirect.github.com/dotnet/extensions/pull/5789) - Fix XML comment by [@​gewarren](https://redirect.github.com/gewarren) in [https://github.com/dotnet/extensions/pull/5790](https://redirect.github.com/dotnet/extensions/pull/5790) - Update HybridCacheOptions.cs (minor typo) by [@​jodydonetti](https://redirect.github.com/jodydonetti) in [https://github.com/dotnet/extensions/pull/5757](https://redirect.github.com/dotnet/extensions/pull/5757) - Ensure Ollama streaming updates specify a CompletionId. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5795](https://redirect.github.com/dotnet/extensions/pull/5795) - Update CHANGELOG.mds for 9.1.0-preview.1.25064.3 by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5797](https://redirect.github.com/dotnet/extensions/pull/5797) - Add Obsolete attribute on IResourceMonitor and its friends by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5774](https://redirect.github.com/dotnet/extensions/pull/5774) - Make a number of improvements to the OpenAI serialization helpers. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5799](https://redirect.github.com/dotnet/extensions/pull/5799) - Add note and sample for the case of using Azure OpenAI by [@​joperezr](https://redirect.github.com/joperezr) in [https://github.com/dotnet/extensions/pull/5802](https://redirect.github.com/dotnet/extensions/pull/5802) - API that allows to remove all resilience handlers from the HTTP client by [@​rainsxng](https://redirect.github.com/rainsxng) in [https://github.com/dotnet/extensions/pull/5801](https://redirect.github.com/dotnet/extensions/pull/5801) - Update OpenTelemtryChatClient/EmbeddingGenerator for 1.30 by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5815](https://redirect.github.com/dotnet/extensions/pull/5815) - Metadata reports generator (Issue [#​3999](https://redirect.github.com/dotnet/extensions/issues/3999)) by [@​IbrahimMNada](https://redirect.github.com/IbrahimMNada) in [https://github.com/dotnet/extensions/pull/5531](https://redirect.github.com/dotnet/extensions/pull/5531) - Ensure the Ollama clients validate HTTP status codes. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5821](https://redirect.github.com/dotnet/extensions/pull/5821) - Fix poor wording in CA2253 error message by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5822](https://redirect.github.com/dotnet/extensions/pull/5822) - Inheritdoc fixes by [@​gewarren](https://redirect.github.com/gewarren) in [https://github.com/dotnet/extensions/pull/5823](https://redirect.github.com/dotnet/extensions/pull/5823) - Remove `ImageContent` and `AudioContent` by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5814](https://redirect.github.com/dotnet/extensions/pull/5814) - Make HealthChecks.ResourceUtilization use observable instruments by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5798](https://redirect.github.com/dotnet/extensions/pull/5798) - Resource Monitoring metrics on Windows - remove multiplication by 100 by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5473](https://redirect.github.com/dotnet/extensions/pull/5473) - Initial chat template by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5837](https://redirect.github.com/dotnet/extensions/pull/5837) - The tested logic uses invariant culture, but unit tests assert against current culture by [@​Demo30](https://redirect.github.com/Demo30) in [https://github.com/dotnet/extensions/pull/5841](https://redirect.github.com/dotnet/extensions/pull/5841) - Fix links in CONTRIBUTING.md by [@​Demo30](https://redirect.github.com/Demo30) in [https://github.com/dotnet/extensions/pull/5840](https://redirect.github.com/dotnet/extensions/pull/5840) - Chat template CR feedback by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5845](https://redirect.github.com/dotnet/extensions/pull/5845) - Chat template: PDF citation viewer by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5843](https://redirect.github.com/dotnet/extensions/pull/5843) - Small fixes for chat template by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5839](https://redirect.github.com/dotnet/extensions/pull/5839) - HybridCache : implement the tag expiration feature by [@​mgravell](https://redirect.github.com/mgravell) in [https://github.com/dotnet/extensions/pull/5785](https://redirect.github.com/dotnet/extensions/pull/5785) #### New Contributors - [@​IbrahimMNada](https://redirect.github.com/IbrahimMNada) made their first contribution in [https://github.com/dotnet/extensions/pull/5531](https://redirect.github.com/dotnet/extensions/pull/5531) - [@​Demo30](https://redirect.github.com/Demo30) made their first contribution in [https://github.com/dotnet/extensions/pull/5841](https://redirect.github.com/dotnet/extensions/pull/5841) **Full Changelog**: https://github.com/dotnet/extensions/compare/v9.1.0...v9.2.0
dotnet/sdk (dotnet-sdk) ### [`v9.0.200`](https://redirect.github.com/dotnet/sdk/compare/v9.0.103...v9.0.200) [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v9.0.103...v9.0.200) ### [`v9.0.103`](https://redirect.github.com/dotnet/sdk/compare/v9.0.102...v9.0.103) [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v9.0.102...v9.0.103)
--- ### 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. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] 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> --- Directory.Packages.props | 4 ++-- global.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3502cc9f..c445681f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,13 +22,13 @@ - + - +
diff --git a/global.json b/global.json index 79ead71d..12741f85 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMajor", - "version": "9.0.102", + "version": "9.0.200", "allowPrerelease": false } } From 4977542515bff302c7a88f3fa301bb129d7ea8cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 09:39:27 +0000 Subject: [PATCH 259/316] chore(deps): update spec digest to a69f748 (#382) 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 | `54952f3` -> `a69f748` | --- ### 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 54952f3b..a69f748d 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 54952f3b545a09ce966a4dbb86c9490a1ce3333b +Subproject commit a69f748db2edfec7015ca6bb702ca22fd8c5ef30 From a98334edfc0a6a14ff60e362bd7aa198b70ff255 Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:32:54 +0000 Subject: [PATCH 260/316] fix: Address issue with newline characters when running Logging Hook Unit Tests on linux (#374) ## This PR I noticed when I ran the LoggingHook unit tests on a WSL Ubuntu instance the some tests fails due to new line characters. It appears I never normalized or excluded newline characters when asserting equal ![image](https://github.com/user-attachments/assets/91bceb1b-41be-4f5c-b330-874ef614c163) I also removed the `Assert.Contains` snippets as the content and layout of the log message is asserted in the other tests. This Assert method does not have an overload for ignoring new line characters I assume the GitHub CI runner may be ignore the line endings by default? ### Related Issues ### Notes ### Follow-up Tasks ### How to test Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --- .../Hooks/LoggingHookTests.cs | 81 +++++++++---------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs index 51ec8cb1..7697d9bd 100644 --- a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs @@ -44,7 +44,9 @@ public async Task BeforeAsync_Without_EvaluationContext_Generates_Debug_Log() DefaultValue:False """, - record.Message); + record.Message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -74,7 +76,9 @@ public async Task BeforeAsync_Without_EvaluationContext_Generates_Correct_Log_Me DefaultValue:False """, - record.Message); + record.Message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -107,15 +111,6 @@ public async Task BeforeAsync_With_EvaluationContext_Generates_Correct_Log_Messa var record = logger.LatestRecord; Assert.Equal(LogLevel.Debug, record.Level); - Assert.Contains( - """ - Before Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - """, - record.Message); Assert.Multiple( () => Assert.Contains("key_1:value", record.Message), () => Assert.Contains("key_2:False", record.Message), @@ -157,7 +152,9 @@ public async Task BeforeAsync_With_No_EvaluationContext_Generates_Correct_Log_Me Context: """, - record.Message); + record.Message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -215,7 +212,9 @@ public async Task ErrorAsync_Without_EvaluationContext_Generates_Correct_Log_Mes DefaultValue:False """, - record.Message); + record.Message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -250,15 +249,6 @@ public async Task ErrorAsync_With_EvaluationContext_Generates_Correct_Log_Messag var record = logger.LatestRecord; Assert.Equal(LogLevel.Error, record.Level); - Assert.Contains( - """ - Error during Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - """, - record.Message); Assert.Multiple( () => Assert.Contains("key_1: ", record.Message), () => Assert.Contains("key_2:True", record.Message), @@ -301,7 +291,9 @@ public async Task ErrorAsync_With_No_EvaluationContext_Generates_Correct_Log_Mes Context: """, - record.Message); + record.Message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -358,7 +350,9 @@ public async Task AfterAsync_Without_EvaluationContext_Generates_Correct_Log_Mes DefaultValue:False """, - record.Message); + record.Message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -393,16 +387,6 @@ public async Task AfterAsync_With_EvaluationContext_Generates_Correct_Log_Messag var record = logger.LatestRecord; Assert.Equal(LogLevel.Debug, record.Level); - Assert.Contains( - """ - After Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - """, - record.Message); - // .NET Framework uses G15 formatter on double.ToString // .NET uses G17 formatter on double.ToString #if NET462 @@ -452,7 +436,9 @@ public async Task AfterAsync_With_No_EvaluationContext_Generates_Correct_Log_Mes Context: """, - record.Message); + record.Message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -499,8 +485,9 @@ public async Task With_Structure_Type_In_Context_Returns_Qualified_Name_Of_Value key_1:OpenFeature.Model.Value """, - message - ); + message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -539,8 +526,9 @@ public async Task Without_Domain_Returns_Missing() key_1:True """, - message - ); + message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -579,8 +567,9 @@ public async Task Without_Provider_Returns_Missing() key_1:True """, - message - ); + message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -619,8 +608,9 @@ public async Task Without_DefaultValue_Returns_Missing() key_1:True """, - message - ); + message, + ignoreLineEndingDifferences: true + ); } [Fact] @@ -659,8 +649,9 @@ public async Task Without_EvaluationContextValue_Returns_Nothing() key_1: """, - message - ); + message, + ignoreLineEndingDifferences: true + ); } private static string NormalizeLogRecord(FakeLogRecord record) From cc2990ff8e7bf5148ab1cd867d9bfabfc0b7af8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:28:22 +0000 Subject: [PATCH 261/316] chore(deps): update github/codeql-action digest to b56ba49 (#384) 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://redirect.github.com/github/codeql-action) | action | digest | `9e8d078` -> `b56ba49` | --- ### 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 5c269e20..dcfa0d60 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@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # 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@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # 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@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 From accf57181b34c600cb775a93b173f644d8c445d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:16:56 +0000 Subject: [PATCH 262/316] chore(deps): update actions/upload-artifact action to v4.6.1 (#385) 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 | |---|---|---|---| | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | patch | `v4.6.0` -> `v4.6.1` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.6.1`](https://redirect.github.com/actions/upload-artifact/releases/tag/v4.6.1) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.6.0...v4.6.1) #### What's Changed - Update to use artifact 2.2.2 package by [@​yacaovsnc](https://redirect.github.com/yacaovsnc) in [https://github.com/actions/upload-artifact/pull/673](https://redirect.github.com/actions/upload-artifact/pull/673) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4...v4.6.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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0967b0f3..378f22d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: nupkgs path: src/**/*.nupkg From 9185b76276857199fef44238143d0742cf491333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:25:46 +0000 Subject: [PATCH 263/316] ci: Add integration tests to the test action (#383) 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> --- .../FeatureFlagIntegrationTest.cs | 17 +++++++++++++---- .../OpenFeature.IntegrationTests.csproj | 5 ++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs index 24561f1a..2f9746eb 100644 --- a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs +++ b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs @@ -27,7 +27,7 @@ public class FeatureFlagIntegrationTest public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string userId, bool expectedResult, ServiceLifetime serviceLifetime) { // Arrange - using var server = await CreateServerAsync(services => + using var server = await CreateServerAsync(serviceLifetime, services => { switch (serviceLifetime) { @@ -59,7 +59,7 @@ public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string us Assert.Equal(expectedResult, responseContent.FeatureValue); } - private static async Task CreateServerAsync(Action? configureServices = null) + private static async Task CreateServerAsync(ServiceLifetime serviceLifetime, Action? configureServices = null) { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); @@ -82,8 +82,17 @@ private static async Task CreateServerAsync(Action { - var flagService = provider.GetRequiredService(); - return flagService.GetFlags(); + if (serviceLifetime == ServiceLifetime.Scoped) + { + using var scoped = provider.CreateScope(); + var flagService = scoped.ServiceProvider.GetRequiredService(); + return flagService.GetFlags(); + } + else + { + var flagService = provider.GetRequiredService(); + return flagService.GetFlags(); + } }); }); diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj index e95aa810..baf5fdfb 100644 --- a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj +++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj @@ -14,7 +14,10 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all +
From c69a6e5d71a6d652017a0d46c8390554a1dec59e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 24 Feb 2025 07:08:08 -0800 Subject: [PATCH 264/316] chore: Correct LoggingHookTest timestamp handling. (#386) ## This PR While working on adding support for hook data I noticed unit tests only run correctly in a UTC+0 timezone. This PR formats the timestamp to match how it will appear in the log. Alternatively the logging hook could be changed to always output UTC. ### Related Issues ### Notes ### Follow-up Tasks ### How to test Unit tests should pass when running in a non-UTC timezone. Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- test/OpenFeature.Tests/Hooks/LoggingHookTests.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs index 7697d9bd..7f299504 100644 --- a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs @@ -89,12 +89,13 @@ public async Task BeforeAsync_With_EvaluationContext_Generates_Correct_Log_Messa var clientMetadata = new ClientMetadata("client", "1.0.0"); var providerMetadata = new Metadata("provider"); + var timestamp = DateTime.Parse("2025-01-01T11:00:00.0000000Z"); var evaluationContext = EvaluationContext.Builder() .Set("key_1", "value") .Set("key_2", false) .Set("key_3", 1.531) .Set("key_4", 42) - .Set("key_5", DateTime.Parse("2025-01-01T11:00:00.0000000Z")) + .Set("key_5", timestamp) .Build(); var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, @@ -116,7 +117,7 @@ public async Task BeforeAsync_With_EvaluationContext_Generates_Correct_Log_Messa () => Assert.Contains("key_2:False", record.Message), () => Assert.Contains("key_3:1.531", record.Message), () => Assert.Contains("key_4:42", record.Message), - () => Assert.Contains("key_5:2025-01-01T11:00:00.0000000+00:00", record.Message) + () => Assert.Contains($"key_5:{timestamp:O}", record.Message) ); } @@ -225,12 +226,14 @@ public async Task ErrorAsync_With_EvaluationContext_Generates_Correct_Log_Messag var clientMetadata = new ClientMetadata("client", "1.0.0"); var providerMetadata = new Metadata("provider"); + + var timestamp = DateTime.Parse("2099-01-01T01:00:00.0000000Z"); var evaluationContext = EvaluationContext.Builder() .Set("key_1", " ") .Set("key_2", true) .Set("key_3", 0.002154) .Set("key_4", -15) - .Set("key_5", DateTime.Parse("2099-01-01T01:00:00.0000000Z")) + .Set("key_5", timestamp) .Build(); var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, @@ -254,7 +257,7 @@ public async Task ErrorAsync_With_EvaluationContext_Generates_Correct_Log_Messag () => Assert.Contains("key_2:True", record.Message), () => Assert.Contains("key_3:0.002154", record.Message), () => Assert.Contains("key_4:-15", record.Message), - () => Assert.Contains("key_5:2099-01-01T01:00:00.0000000+00:00", record.Message) + () => Assert.Contains($"key_5:{timestamp:O}", record.Message) ); } From 63846ad1033399e9c84ad5946367c5eef2663b5b Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Mon, 24 Feb 2025 10:52:08 -0500 Subject: [PATCH 265/316] chore: update release please repo, specify action permissions (#369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michael Beemer Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/release.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20f4b9ec..221044ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,12 +5,18 @@ on: branches: - main +permissions: + contents: read + jobs: release-please: + permissions: + contents: write # for googleapis/release-please-action to create release commit + pull-requests: write # for googleapis/release-please-action to create release PR runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 + - uses: googleapis/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 id: release with: command: manifest @@ -52,6 +58,8 @@ jobs: sbom: runs-on: ubuntu-latest + permissions: + contents: write # upload sbom to a release needs: release-please continue-on-error: true if: ${{ needs.release-please.outputs.release_created }} From 85075ac7f46783dd1bcfdbbe6bd10d81eb9adb8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:27:15 +0000 Subject: [PATCH 266/316] chore(deps): update spec digest to 0cd553d (#389) 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 | `a69f748` -> `0cd553d` | --- ### 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 a69f748d..0cd553d8 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit a69f748db2edfec7015ca6bb702ca22fd8c5ef30 +Subproject commit 0cd553d85f03b0b7ab983a988ce1720a3190bc88 From 4ce2b95256fec96cb6bd720aeda16f2e11f53f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:59:12 +0000 Subject: [PATCH 267/316] test: Add missing gherkin tests (#390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes several changes aimed at improving the test setup, enhancing the metadata handling, and refactoring the code for better readability and maintainability. ### Test Setup Improvements: * [`.github/workflows/e2e.yml`](diffhunk://#diff-3e103440521ada06efd263ae09b259e5507e4b8f7408308dc227621ad9efa31eL34-R34): Updated the `Initialize Tests` step to copy all Gherkin feature files instead of just `evaluation.feature`. * [`test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj`](diffhunk://#diff-ab2ad60395e1cc72b327459243ed8c5711efbd88531a3b3b813fb6c4c6019886R32-R38): Added a new `GherkinRestore` target to copy Gherkin files before the build. ### Metadata Handling Enhancements: * [`src/OpenFeature/Model/ImmutableMetadata.cs`](diffhunk://#diff-bf57504fda39c85dbda91d3829514b8b014f1293e5b5e53e490f4f4b74f0905bR88-R89): Added an internal `Count` property to the `ImmutableMetadata` class. * [`src/OpenFeature/Providers/Memory/Flag.cs`](diffhunk://#diff-ba67e6465022bead062d71042b8f5d67b47ec11e06c4eefe2b9f473ac34fc8d1R22-R36): Introduced an optional `flagMetadata` parameter to the `Flag` class and updated the `Evaluate` method to include `flagMetadata` in the `ResolutionDetails`. [[1]](diffhunk://#diff-ba67e6465022bead062d71042b8f5d67b47ec11e06c4eefe2b9f473ac34fc8d1R22-R36) [[2]](diffhunk://#diff-ba67e6465022bead062d71042b8f5d67b47ec11e06c4eefe2b9f473ac34fc8d1L47-R51) [[3]](diffhunk://#diff-ba67e6465022bead062d71042b8f5d67b47ec11e06c4eefe2b9f473ac34fc8d1L68-R73) ### Code Refactoring: * [`build/Common.tests.props`](diffhunk://#diff-5472aa271be4e6ac0c793a3c1b9226e4f9a7907a6baa99ea16542fb89107ae86L8-R12): Simplified the condition for identifying test projects by removing the dot before 'Tests'. * [`test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs`](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL10-R103): Refactored the class to use private fields with underscore prefixes for better readability and consistency. [[1]](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL10-R103) [[2]](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL115-R118) [[3]](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL130-R133) [[4]](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL145-R148) [[5]](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL160-R163) [[6]](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL175-R178) [[7]](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL190-R194) ### Related Issues Fixes #376 ### Follow-up Tasks - Cleanup `test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs` file. It should apply the same patterns as discussed in the PR. See #391 --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/e2e.yml | 2 +- .gitignore | 4 +- build/Common.tests.props | 4 +- build/xunit.runner.json | 5 +- src/OpenFeature/Model/ImmutableMetadata.cs | 2 + src/OpenFeature/Providers/Memory/Flag.cs | 11 +- .../OpenFeature.E2ETests.csproj | 7 + .../Steps/BaseStepDefinitions.cs | 124 ++++++++++++++++ .../Steps/EvaluationStepDefinitions.cs | 135 +++++++++--------- .../Steps/HooksStepDefinitions.cs | 132 +++++++++++++++++ .../Steps/MetadataStepDefinitions.cs | 84 +++++++++++ .../Utils/FlagTypesUtil.cs | 28 ++++ test/OpenFeature.E2ETests/Utils/TestHook.cs | 54 +++++++ .../ImmutableMetadataTest.cs | 31 ++++ 14 files changed, 544 insertions(+), 79 deletions(-) create mode 100644 test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs create mode 100644 test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs create mode 100644 test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs create mode 100644 test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs create mode 100644 test/OpenFeature.E2ETests/Utils/TestHook.cs diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b1458b8b..7425a82c 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -31,7 +31,7 @@ jobs: - name: Initialize Tests run: | git submodule update --init --recursive - cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ + cp spec/specification/assets/gherkin/*.feature test/OpenFeature.E2ETests/Features/ - name: Run Tests run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions diff --git a/.gitignore b/.gitignore index a44a107a..055ffe50 100644 --- a/.gitignore +++ b/.gitignore @@ -350,7 +350,7 @@ ASALocalRun/ !.vscode/extensions.json # integration tests -test/OpenFeature.E2ETests/Features/evaluation.feature -test/OpenFeature.E2ETests/Features/evaluation.feature.cs +test/OpenFeature.E2ETests/Features/*.feature +test/OpenFeature.E2ETests/Features/*.feature.cs cs-report.json specification.json diff --git a/build/Common.tests.props b/build/Common.tests.props index 590fc99d..8ea5c27d 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -5,11 +5,11 @@ false - + true - + PreserveNewest diff --git a/build/xunit.runner.json b/build/xunit.runner.json index c4dcd538..47a03b98 100644 --- a/build/xunit.runner.json +++ b/build/xunit.runner.json @@ -1,4 +1,5 @@ { "maxParallelThreads": 1, - "parallelizeTestCollections": false -} \ No newline at end of file + "parallelizeTestCollections": false, + "parallelizeAssembly": false +} diff --git a/src/OpenFeature/Model/ImmutableMetadata.cs b/src/OpenFeature/Model/ImmutableMetadata.cs index 1f2c6f8a..f1d54449 100644 --- a/src/OpenFeature/Model/ImmutableMetadata.cs +++ b/src/OpenFeature/Model/ImmutableMetadata.cs @@ -85,4 +85,6 @@ public ImmutableMetadata(Dictionary metadata) return value is T tValue ? tValue : null; } + + internal int Count => this._metadata.Count; } diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 5cee86ea..7e125a89 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -19,6 +19,7 @@ public sealed class Flag : Flag private readonly Dictionary _variants; private readonly string _defaultVariant; private readonly Func? _contextEvaluator; + private readonly ImmutableMetadata? _flagMetadata; /// /// Flag representation for the in-memory provider. @@ -26,11 +27,13 @@ public sealed class Flag : Flag /// dictionary of variants and their corresponding values /// default variant (should match 1 key in variants dictionary) /// optional context-sensitive evaluation function - public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null) + /// optional metadata for the flag + public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null, ImmutableMetadata? flagMetadata = null) { this._variants = variants; this._defaultVariant = defaultVariant; this._contextEvaluator = contextEvaluator; + this._flagMetadata = flagMetadata; } internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) @@ -44,7 +47,8 @@ internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? e flagKey, value, variant: this._defaultVariant, - reason: Reason.Static + reason: Reason.Static, + flagMetadata: this._flagMetadata ); } else @@ -65,7 +69,8 @@ internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? e flagKey, value, variant: variant, - reason: Reason.TargetingMatch + reason: Reason.TargetingMatch, + flagMetadata: this._flagMetadata ); } } diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 2d78a9ed..0d5ed8ce 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -29,4 +29,11 @@ + + + + + + + diff --git a/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs new file mode 100644 index 00000000..c19a5a59 --- /dev/null +++ b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using OpenFeature.E2ETests.Utils; +using OpenFeature.Model; +using OpenFeature.Providers.Memory; +using Reqnroll; + +namespace OpenFeature.E2ETests.Steps; + +[Binding] +public class BaseStepDefinitions +{ + internal FeatureClient? Client; + private string _flagKey = null!; + private string _defaultValue = null!; + internal FlagType? FlagTypeEnum; + internal object Result = null!; + + [Given(@"a stable provider")] + public void GivenAStableProvider() + { + var memProvider = new InMemoryProvider(E2EFlagConfig); + Api.Instance.SetProviderAsync(memProvider).Wait(); + this.Client = Api.Instance.GetClient("TestClient", "1.0.0"); + } + + [Given(@"a Boolean-flag with key ""(.*)"" and a default value ""(.*)""")] + [Given(@"a boolean-flag with key ""(.*)"" and a default value ""(.*)""")] + public void GivenABoolean_FlagWithKeyAndADefaultValue(string key, string defaultType) + { + this._flagKey = key; + this._defaultValue = defaultType; + this.FlagTypeEnum = FlagType.Boolean; + } + + [Given(@"a Float-flag with key ""(.*)"" and a default value ""(.*)""")] + [Given(@"a float-flag with key ""(.*)"" and a default value ""(.*)""")] + public void GivenAFloat_FlagWithKeyAndADefaultValue(string key, string defaultType) + { + this._flagKey = key; + this._defaultValue = defaultType; + this.FlagTypeEnum = FlagType.Float; + } + + [Given(@"a Integer-flag with key ""(.*)"" and a default value ""(.*)""")] + [Given(@"a integer-flag with key ""(.*)"" and a default value ""(.*)""")] + public void GivenAnInteger_FlagWithKeyAndADefaultValue(string key, string defaultType) + { + this._flagKey = key; + this._defaultValue = defaultType; + this.FlagTypeEnum = FlagType.Integer; + } + + [Given(@"a String-flag with key ""(.*)"" and a default value ""(.*)""")] + [Given(@"a string-flag with key ""(.*)"" and a default value ""(.*)""")] + public void GivenAString_FlagWithKeyAndADefaultValue(string key, string defaultType) + { + this._flagKey = key; + this._defaultValue = defaultType; + this.FlagTypeEnum = FlagType.String; + } + + [When(@"the flag was evaluated with details")] + public async Task WhenTheFlagWasEvaluatedWithDetails() + { + switch (this.FlagTypeEnum) + { + case FlagType.Boolean: + this.Result = await this.Client! + .GetBooleanDetailsAsync(this._flagKey, bool.Parse(this._defaultValue)).ConfigureAwait(false); + break; + case FlagType.Float: + this.Result = await this.Client! + .GetDoubleDetailsAsync(this._flagKey, double.Parse(this._defaultValue)).ConfigureAwait(false); + break; + case FlagType.Integer: + this.Result = await this.Client! + .GetIntegerDetailsAsync(this._flagKey, int.Parse(this._defaultValue)).ConfigureAwait(false); + break; + case FlagType.String: + this.Result = await this.Client!.GetStringDetailsAsync(this._flagKey, this._defaultValue) + .ConfigureAwait(false); + break; + } + } + + private static readonly IDictionary E2EFlagConfig = new Dictionary + { + { + "metadata-flag", new Flag( + variants: new Dictionary { { "on", true }, { "off", false } }, + defaultVariant: "on", + flagMetadata: new ImmutableMetadata(new Dictionary + { + { "string", "1.0.2" }, { "integer", 2 }, { "float", 0.1 }, { "boolean", true } + }) + ) + }, + { + "boolean-flag", new Flag( + variants: new Dictionary { { "on", true }, { "off", false } }, + defaultVariant: "on" + ) + }, + { + "integer-flag", new Flag( + variants: new Dictionary { { "23", 23 }, { "42", 42 } }, + defaultVariant: "23" + ) + }, + { + "float-flag", new Flag( + variants: new Dictionary { { "2.3", 2.3 }, { "4.2", 4.2 } }, + defaultVariant: "2.3" + ) + }, + { + "string-flag", new Flag( + variants: new Dictionary { { "value", "value" }, { "value2", "value2" } }, + defaultVariant: "value" + ) + } + }; +} diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index b7e60bcb..2a375d23 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -7,103 +7,100 @@ using Reqnroll; using Xunit; -namespace OpenFeature.E2ETests +namespace OpenFeature.E2ETests.Steps { [Binding] + [Scope(Feature = "Flag evaluation")] public class EvaluationStepDefinitions { - private static FeatureClient? client; - private Task? booleanFlagValue; - private Task? stringFlagValue; - private Task? intFlagValue; - private Task? doubleFlagValue; - private Task? objectFlagValue; - private Task>? booleanFlagDetails; - private Task>? stringFlagDetails; - private Task>? intFlagDetails; - private Task>? doubleFlagDetails; - private Task>? objectFlagDetails; - private string? contextAwareFlagKey; - private string? contextAwareDefaultValue; - private string? contextAwareValue; - private EvaluationContext? context; - private string? notFoundFlagKey; - private string? notFoundDefaultValue; - private FlagEvaluationDetails? notFoundDetails; - private string? typeErrorFlagKey; - private int typeErrorDefaultValue; - private FlagEvaluationDetails? typeErrorDetails; - - public EvaluationStepDefinitions(ScenarioContext scenarioContext) - { - } + private FeatureClient? _client; + private Task? _booleanFlagValue; + private Task? _stringFlagValue; + private Task? _intFlagValue; + private Task? _doubleFlagValue; + private Task? _objectFlagValue; + private Task>? _booleanFlagDetails; + private Task>? _stringFlagDetails; + private Task>? _intFlagDetails; + private Task>? _doubleFlagDetails; + private Task>? _objectFlagDetails; + private string? _contextAwareFlagKey; + private string? _contextAwareDefaultValue; + private string? _contextAwareValue; + private EvaluationContext? _context; + private string? _notFoundFlagKey; + private string? _notFoundDefaultValue; + private FlagEvaluationDetails? _notFoundDetails; + private string? _typeErrorFlagKey; + private int _typeErrorDefaultValue; + private FlagEvaluationDetails? _typeErrorDetails; [Given("a stable provider")] public void GivenAStableProvider() { - var memProvider = new InMemoryProvider(this.e2eFlagConfig); + var memProvider = new InMemoryProvider(this._e2EFlagConfig); Api.Instance.SetProviderAsync(memProvider).Wait(); - client = Api.Instance.GetClient("TestClient", "1.0.0"); + this._client = Api.Instance.GetClient("TestClient", "1.0.0"); } [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagValue = client?.GetBooleanValueAsync(flagKey, defaultValue); + this._booleanFlagValue = this._client?.GetBooleanValueAsync(flagKey, defaultValue); } [Then(@"the resolved boolean value should be ""(.*)""")] public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) { - Assert.Equal(expectedValue, this.booleanFlagValue?.Result); + Assert.Equal(expectedValue, this._booleanFlagValue?.Result); } [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - this.stringFlagValue = client?.GetStringValueAsync(flagKey, defaultValue); + this._stringFlagValue = this._client?.GetStringValueAsync(flagKey, defaultValue); } [Then(@"the resolved string value should be ""(.*)""")] public void Thentheresolvedstringvalueshouldbe(string expected) { - Assert.Equal(expected, this.stringFlagValue?.Result); + Assert.Equal(expected, this._stringFlagValue?.Result); } [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) { - this.intFlagValue = client?.GetIntegerValueAsync(flagKey, defaultValue); + this._intFlagValue = this._client?.GetIntegerValueAsync(flagKey, defaultValue); } [Then(@"the resolved integer value should be (.*)")] public void Thentheresolvedintegervalueshouldbe(int expected) { - Assert.Equal(expected, this.intFlagValue?.Result); + Assert.Equal(expected, this._intFlagValue?.Result); } [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagValue = client?.GetDoubleValueAsync(flagKey, defaultValue); + this._doubleFlagValue = this._client?.GetDoubleValueAsync(flagKey, defaultValue); } [Then(@"the resolved float value should be (.*)")] public void Thentheresolvedfloatvalueshouldbe(double expected) { - Assert.Equal(expected, this.doubleFlagValue?.Result); + Assert.Equal(expected, this._doubleFlagValue?.Result); } [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) { - this.objectFlagValue = client?.GetObjectValueAsync(flagKey, new Value()); + this._objectFlagValue = this._client?.GetObjectValueAsync(flagKey, new Value()); } [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) { - Value? value = this.objectFlagValue?.Result; + Value? value = this._objectFlagValue?.Result; Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); @@ -112,13 +109,13 @@ public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespe [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) { - this.booleanFlagDetails = client?.GetBooleanDetailsAsync(flagKey, defaultValue); + this._booleanFlagDetails = this._client?.GetBooleanDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(bool expectedValue, string expectedVariant, string expectedReason) { - var result = this.booleanFlagDetails?.Result; + var result = this._booleanFlagDetails?.Result; Assert.Equal(expectedValue, result?.Value); Assert.Equal(expectedVariant, result?.Variant); Assert.Equal(expectedReason, result?.Reason); @@ -127,13 +124,13 @@ public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthere [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] public void Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) { - this.stringFlagDetails = client?.GetStringDetailsAsync(flagKey, defaultValue); + this._stringFlagDetails = this._client?.GetStringDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(string expectedValue, string expectedVariant, string expectedReason) { - var result = this.stringFlagDetails?.Result; + var result = this._stringFlagDetails?.Result; Assert.Equal(expectedValue, result?.Value); Assert.Equal(expectedVariant, result?.Variant); Assert.Equal(expectedReason, result?.Reason); @@ -142,13 +139,13 @@ public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandtherea [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) { - this.intFlagDetails = client?.GetIntegerDetailsAsync(flagKey, defaultValue); + this._intFlagDetails = this._client?.GetIntegerDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(int expectedValue, string expectedVariant, string expectedReason) { - var result = this.intFlagDetails?.Result; + var result = this._intFlagDetails?.Result; Assert.Equal(expectedValue, result?.Value); Assert.Equal(expectedVariant, result?.Variant); Assert.Equal(expectedReason, result?.Reason); @@ -157,13 +154,13 @@ public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthere [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] public void Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) { - this.doubleFlagDetails = client?.GetDoubleDetailsAsync(flagKey, defaultValue); + this._doubleFlagDetails = this._client?.GetDoubleDetailsAsync(flagKey, defaultValue); } [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(double expectedValue, string expectedVariant, string expectedReason) { - var result = this.doubleFlagDetails?.Result; + var result = this._doubleFlagDetails?.Result; Assert.Equal(expectedValue, result?.Value); Assert.Equal(expectedVariant, result?.Variant); Assert.Equal(expectedReason, result?.Reason); @@ -172,13 +169,13 @@ public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereas [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] public void Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) { - this.objectFlagDetails = client?.GetObjectDetailsAsync(flagKey, new Value()); + this._objectFlagDetails = this._client?.GetObjectDetailsAsync(flagKey, new Value()); } [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] public void Thentheresolvedobjectdetailsvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) { - var value = this.objectFlagDetails?.Result.Value; + var value = this._objectFlagDetails?.Result.Value; Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); @@ -187,14 +184,14 @@ public void Thentheresolvedobjectdetailsvalueshouldbecontainfieldsandwithvaluesa [Then(@"the variant should be ""(.*)"", and the reason should be ""(.*)""")] public void Giventhevariantshouldbeandthereasonshouldbe(string expectedVariant, string expectedReason) { - Assert.Equal(expectedVariant, this.objectFlagDetails?.Result.Variant); - Assert.Equal(expectedReason, this.objectFlagDetails?.Result.Reason); + Assert.Equal(expectedVariant, this._objectFlagDetails?.Result.Variant); + Assert.Equal(expectedReason, this._objectFlagDetails?.Result.Reason); } [When(@"context contains keys ""(.*)"", ""(.*)"", ""(.*)"", ""(.*)"" with values ""(.*)"", ""(.*)"", (.*), ""(.*)""")] public void Whencontextcontainskeyswithvalues(string field1, string field2, string field3, string field4, string value1, string value2, int value3, string value4) { - this.context = new EvaluationContextBuilder() + this._context = new EvaluationContextBuilder() .Set(field1, value1) .Set(field2, value2) .Set(field3, value3) @@ -204,67 +201,67 @@ public void Whencontextcontainskeyswithvalues(string field1, string field2, stri [When(@"a flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - this.contextAwareFlagKey = flagKey; - this.contextAwareDefaultValue = defaultValue; - this.contextAwareValue = client?.GetStringValueAsync(flagKey, this.contextAwareDefaultValue, this.context)?.Result; + this._contextAwareFlagKey = flagKey; + this._contextAwareDefaultValue = defaultValue; + this._contextAwareValue = this._client?.GetStringValueAsync(flagKey, this._contextAwareDefaultValue, this._context).Result; } [Then(@"the resolved string response should be ""(.*)""")] public void Thentheresolvedstringresponseshouldbe(string expected) { - Assert.Equal(expected, this.contextAwareValue); + Assert.Equal(expected, this._contextAwareValue); } [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string? emptyContextValue = client?.GetStringValueAsync(this.contextAwareFlagKey!, this.contextAwareDefaultValue!, EvaluationContext.Empty).Result; + string? emptyContextValue = this._client?.GetStringValueAsync(this._contextAwareFlagKey!, this._contextAwareDefaultValue!, EvaluationContext.Empty).Result; Assert.Equal(expected, emptyContextValue); } [When(@"a non-existent string flag with key ""(.*)"" is evaluated with details and a default value ""(.*)""")] public void Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultvalue(string flagKey, string defaultValue) { - this.notFoundFlagKey = flagKey; - this.notFoundDefaultValue = defaultValue; - this.notFoundDetails = client?.GetStringDetailsAsync(this.notFoundFlagKey, this.notFoundDefaultValue).Result; + this._notFoundFlagKey = flagKey; + this._notFoundDefaultValue = defaultValue; + this._notFoundDetails = this._client?.GetStringDetailsAsync(this._notFoundFlagKey, this._notFoundDefaultValue).Result; } [Then(@"the default string value should be returned")] public void Thenthedefaultstringvalueshouldbereturned() { - Assert.Equal(this.notFoundDefaultValue, this.notFoundDetails?.Value); + Assert.Equal(this._notFoundDefaultValue, this._notFoundDetails?.Value); } [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), this.notFoundDetails?.Reason); - Assert.Equal(errorCode, this.notFoundDetails?.ErrorType.GetDescription()); + Assert.Equal(Reason.Error, this._notFoundDetails?.Reason); + Assert.Equal(errorCode, this._notFoundDetails?.ErrorType.GetDescription()); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] public void Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultvalue(string flagKey, int defaultValue) { - this.typeErrorFlagKey = flagKey; - this.typeErrorDefaultValue = defaultValue; - this.typeErrorDetails = client?.GetIntegerDetailsAsync(this.typeErrorFlagKey, this.typeErrorDefaultValue).Result; + this._typeErrorFlagKey = flagKey; + this._typeErrorDefaultValue = defaultValue; + this._typeErrorDetails = this._client?.GetIntegerDetailsAsync(this._typeErrorFlagKey, this._typeErrorDefaultValue).Result; } [Then(@"the default integer value should be returned")] public void Thenthedefaultintegervalueshouldbereturned() { - Assert.Equal(this.typeErrorDefaultValue, this.typeErrorDetails?.Value); + Assert.Equal(this._typeErrorDefaultValue, this._typeErrorDetails?.Value); } [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), this.typeErrorDetails?.Reason); - Assert.Equal(errorCode, this.typeErrorDetails?.ErrorType.GetDescription()); + Assert.Equal(Reason.Error, this._typeErrorDetails?.Reason); + Assert.Equal(errorCode, this._typeErrorDetails?.ErrorType.GetDescription()); } - private IDictionary e2eFlagConfig = new Dictionary(){ + private readonly IDictionary _e2EFlagConfig = new Dictionary(){ { "boolean-flag", new Flag( variants: new Dictionary(){ diff --git a/test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs new file mode 100644 index 00000000..32e85679 --- /dev/null +++ b/test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs @@ -0,0 +1,132 @@ +using OpenFeature.E2ETests.Utils; +using Reqnroll; +using Xunit; + +namespace OpenFeature.E2ETests.Steps; + +[Binding] +[Scope(Feature = "Evaluation details through hooks")] +public class HooksStepDefinitions : BaseStepDefinitions +{ + private TestHook? _testHook; + + [Given(@"a client with added hook")] + public void GivenAClientWithAddedHook() + { + this._testHook = new TestHook(); + this.Client!.AddHooks(this._testHook); + } + + [Then(@"the ""(.*)"" hook should have been executed")] + public void ThenTheHookShouldHaveBeenExecuted(string hook) + { + this.CheckHookExecution(hook); + } + + [Then(@"the ""(.*)"" hooks should be called with evaluation details")] + public void ThenTheHooksShouldBeCalledWithEvaluationDetails(string hook, Table table) + { + this.CheckHookExecution(hook); + var key = table.Rows[0]["value"]; + switch (key) + { + case "boolean-flag": + CheckCorrectFlag(table); + break; + case "missing-flag": + CheckMissingFlag(table); + break; + case "wrong-flag": + this.CheckWrongFlag(table); + break; + } + } + + private static void CheckCorrectFlag(Table table) + { + Assert.Equal("string", table.Rows[0]["data_type"]); + Assert.Equal("flag_key", table.Rows[0]["key"]); + Assert.Equal("boolean-flag", table.Rows[0]["value"]); + + Assert.Equal("boolean", table.Rows[1]["data_type"]); + Assert.Equal("value", table.Rows[1]["key"]); + Assert.Equal("true", table.Rows[1]["value"]); + + Assert.Equal("string", table.Rows[2]["data_type"]); + Assert.Equal("variant", table.Rows[2]["key"]); + Assert.Equal("on", table.Rows[2]["value"]); + + Assert.Equal("string", table.Rows[3]["data_type"]); + Assert.Equal("reason", table.Rows[3]["key"]); + Assert.Equal("STATIC", table.Rows[3]["value"]); + + Assert.Equal("string", table.Rows[4]["data_type"]); + Assert.Equal("error_code", table.Rows[4]["key"]); + Assert.Equal("null", table.Rows[4]["value"]); + } + + private static void CheckMissingFlag(Table table) + { + Assert.Equal("string", table.Rows[0]["data_type"]); + Assert.Equal("flag_key", table.Rows[0]["key"]); + Assert.Equal("missing-flag", table.Rows[0]["value"]); + + Assert.Equal("string", table.Rows[1]["data_type"]); + Assert.Equal("value", table.Rows[1]["key"]); + Assert.Equal("uh-oh", table.Rows[1]["value"]); + + Assert.Equal("string", table.Rows[2]["data_type"]); + Assert.Equal("variant", table.Rows[2]["key"]); + Assert.Equal("null", table.Rows[2]["value"]); + + Assert.Equal("string", table.Rows[3]["data_type"]); + Assert.Equal("reason", table.Rows[3]["key"]); + Assert.Equal("ERROR", table.Rows[3]["value"]); + + Assert.Equal("string", table.Rows[4]["data_type"]); + Assert.Equal("error_code", table.Rows[4]["key"]); + Assert.Equal("FLAG_NOT_FOUND", table.Rows[4]["value"]); + } + + private void CheckWrongFlag(Table table) + { + Assert.Equal("string", table.Rows[0]["data_type"]); + Assert.Equal("flag_key", table.Rows[0]["key"]); + Assert.Equal("wrong-flag", table.Rows[0]["value"]); + + Assert.Equal("boolean", table.Rows[1]["data_type"]); + Assert.Equal("value", table.Rows[1]["key"]); + Assert.Equal("false", table.Rows[1]["value"]); + + Assert.Equal("string", table.Rows[2]["data_type"]); + Assert.Equal("variant", table.Rows[2]["key"]); + Assert.Equal("null", table.Rows[2]["value"]); + + Assert.Equal("string", table.Rows[3]["data_type"]); + Assert.Equal("reason", table.Rows[3]["key"]); + Assert.Equal("ERROR", table.Rows[3]["value"]); + + Assert.Equal("string", table.Rows[4]["data_type"]); + Assert.Equal("error_code", table.Rows[4]["key"]); + Assert.Equal("TYPE_MISMATCH", table.Rows[4]["value"]); + } + + private void CheckHookExecution(string hook) + { + switch (hook) + { + case "before": + Assert.Equal(1, this._testHook!.BeforeCount); + break; + case "after": + Assert.Equal(1, this._testHook!.AfterCount); + break; + case "error": + Assert.Equal(1, this._testHook!.ErrorCount); + break; + case "finally": + Assert.Equal(1, this._testHook!.FinallyCount); + break; + } + } +} diff --git a/test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs new file mode 100644 index 00000000..7b90990b --- /dev/null +++ b/test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using OpenFeature.E2ETests.Utils; +using OpenFeature.Model; +using Reqnroll; +using Xunit; + +namespace OpenFeature.E2ETests.Steps; + +[Binding] +[Scope(Feature = "Metadata")] +public class MetadataStepDefinitions : BaseStepDefinitions +{ + [Then("the resolved metadata should contain")] + [Scope(Scenario = "Returns metadata")] + public void ThenTheResolvedMetadataShouldContain(DataTable itemsTable) + { + var items = itemsTable.Rows.Select(row => new DataTableRows(row["key"], row["value"], row["metadata_type"])).ToList(); + var metadata = (this.Result as FlagEvaluationDetails)?.FlagMetadata; + + foreach (var item in items) + { + var key = item.Key; + var value = item.Value; + var metadataType = item.MetadataType; + + string? actual = null!; + switch (metadataType) + { + case FlagType.Boolean: + actual = metadata!.GetBool(key).ToString(); + break; + case FlagType.Integer: + actual = metadata!.GetInt(key).ToString(); + break; + case FlagType.Float: + actual = metadata!.GetDouble(key).ToString(); + break; + case FlagType.String: + actual = metadata!.GetString(key); + break; + } + + Assert.Equal(value.ToLowerInvariant(), actual?.ToLowerInvariant()); + } + } + + [Then("the resolved metadata is empty")] + public void ThenTheResolvedMetadataIsEmpty() + { + switch (this.FlagTypeEnum) + { + case FlagType.Boolean: + Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + break; + case FlagType.Float: + Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + break; + case FlagType.Integer: + Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + break; + case FlagType.String: + Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private class DataTableRows + { + public DataTableRows(string key, string value, string metadataType) + { + this.Key = key; + this.Value = value; + + this.MetadataType = FlagTypesUtil.ToEnum(metadataType); + } + + public string Key { get; } + public string Value { get; } + public FlagType MetadataType { get; } + } +} diff --git a/test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs b/test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs new file mode 100644 index 00000000..aa5c91dd --- /dev/null +++ b/test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature.E2ETests.Utils; + +[ExcludeFromCodeCoverage] +internal static class FlagTypesUtil +{ + internal static FlagType ToEnum(string flagType) + { + return flagType.ToLowerInvariant() switch + { + "boolean" => FlagType.Boolean, + "float" => FlagType.Float, + "integer" => FlagType.Integer, + "string" => FlagType.String, + _ => throw new ArgumentException("Invalid flag type") + }; + } +} + +internal enum FlagType +{ + Integer, + Float, + String, + Boolean +} diff --git a/test/OpenFeature.E2ETests/Utils/TestHook.cs b/test/OpenFeature.E2ETests/Utils/TestHook.cs new file mode 100644 index 00000000..7fd204f5 --- /dev/null +++ b/test/OpenFeature.E2ETests/Utils/TestHook.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Model; + +namespace OpenFeature.E2ETests.Utils; + +[ExcludeFromCodeCoverage] +internal class TestHook : Hook +{ + private int _afterCount; + private int _beforeCount; + private int _errorCount; + private int _finallyCount; + + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) + { + this._afterCount++; + return base.AfterAsync(context, details, hints, cancellationToken); + } + + public override ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) + { + this._errorCount++; + return base.ErrorAsync(context, error, hints, cancellationToken); + } + + public override ValueTask FinallyAsync(HookContext context, FlagEvaluationDetails evaluationDetails, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) + { + this._finallyCount++; + return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken); + } + + public override ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) + { + this._beforeCount++; + return base.BeforeAsync(context, hints, cancellationToken); + } + + public int AfterCount => this._afterCount; + public int BeforeCount => this._beforeCount; + public int ErrorCount => this._errorCount; + public int FinallyCount => this._finallyCount; +} diff --git a/test/OpenFeature.Tests/ImmutableMetadataTest.cs b/test/OpenFeature.Tests/ImmutableMetadataTest.cs index 344392b0..cd2fd1d8 100644 --- a/test/OpenFeature.Tests/ImmutableMetadataTest.cs +++ b/test/OpenFeature.Tests/ImmutableMetadataTest.cs @@ -241,4 +241,35 @@ public void GetString_Should_Throw_Value_Is_Invalid() // Assert Assert.Null(result); } + + [Fact] + public void Count_ShouldReturnCountOfMetadata() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", new object() + }, + { + "stringKey", "11" + }, + { + "doubleKey", 1.2 + }, + { + "intKey", 1 + }, + { + "boolKey", true + } + }; + var flagMetadata = new ImmutableMetadata(metadata); + + // Act + var result = flagMetadata.Count; + + // Assert + Assert.Equal(metadata.Count, result); + } } From 06e4e3a7ee11aff5c53eeba2259a840956bc4d5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:59:58 +0000 Subject: [PATCH 268/316] chore(deps): update codecov/codecov-action action to v5.4.0 (#392) 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://redirect.github.com/codecov/codecov-action) | action | minor | `v5.3.1` -> `v5.4.0` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v5.4.0`](https://redirect.github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v540) [Compare Source](https://redirect.github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0) ##### What's Changed - update wrapper submodule to 0.2.0, add recurse_submodules arg by [@​matt-codecov](https://redirect.github.com/matt-codecov) in [https://github.com/codecov/codecov-action/pull/1780](https://redirect.github.com/codecov/codecov-action/pull/1780) - build(deps): bump actions/upload-artifact from 4.6.0 to 4.6.1 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1775](https://redirect.github.com/codecov/codecov-action/pull/1775) - build(deps): bump ossf/scorecard-action from 2.4.0 to 2.4.1 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1776](https://redirect.github.com/codecov/codecov-action/pull/1776) - build(deps): bump github/codeql-action from 3.28.9 to 3.28.10 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1777](https://redirect.github.com/codecov/codecov-action/pull/1777) - Clarify in README that `use_pypi` bypasses integrity checks too by [@​webknjaz](https://redirect.github.com/webknjaz) in [https://github.com/codecov/codecov-action/pull/1773](https://redirect.github.com/codecov/codecov-action/pull/1773) - Fix use of safe.directory inside containers by [@​Flamefire](https://redirect.github.com/Flamefire) in [https://github.com/codecov/codecov-action/pull/1768](https://redirect.github.com/codecov/codecov-action/pull/1768) - Fix description for report_type input by [@​craigscott-crascit](https://redirect.github.com/craigscott-crascit) in [https://github.com/codecov/codecov-action/pull/1770](https://redirect.github.com/codecov/codecov-action/pull/1770) - build(deps): bump github/codeql-action from 3.28.8 to 3.28.9 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1765](https://redirect.github.com/codecov/codecov-action/pull/1765) - Fix a typo in the example by [@​miranska](https://redirect.github.com/miranska) in [https://github.com/codecov/codecov-action/pull/1758](https://redirect.github.com/codecov/codecov-action/pull/1758) - build(deps): bump github/codeql-action from 3.28.5 to 3.28.8 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1757](https://redirect.github.com/codecov/codecov-action/pull/1757) - build(deps): bump github/codeql-action from 3.28.1 to 3.28.5 by [@​app/dependabot](https://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1753](https://redirect.github.com/codecov/codecov-action/pull/1753) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v5.3.1..v5.4.0
--- ### 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 988e9051..03f61e8f 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -36,7 +36,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 + - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From b30350bd49f4a8709b69a3eb2db1152d5a4b7f6c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:33:25 +0000 Subject: [PATCH 269/316] chore(deps): update dependency reqnroll.xunit to 2.4.0 (#396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Reqnroll.xUnit](https://www.reqnroll.net/) ([source](https://redirect.github.com/reqnroll/Reqnroll)) | `2.3.0` -> `2.4.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Reqnroll.xUnit/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Reqnroll.xUnit/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Reqnroll.xUnit/2.3.0/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Reqnroll.xUnit/2.3.0/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
reqnroll/Reqnroll (Reqnroll.xUnit) ### [`v2.4.0`](https://redirect.github.com/reqnroll/Reqnroll/blob/HEAD/CHANGELOG.md#v240---2025-03-06) #### Improvements: - Microsoft.Extensions.DependencyInjection.ReqnrollPlugin: Improved message when \[ScenarioDependencies] can't be found or has an incorrect return type ([#​494](https://redirect.github.com/reqnroll/Reqnroll/issues/494)) - Include original exception for binding errors (`BindingException`) ([#​513](https://redirect.github.com/reqnroll/Reqnroll/issues/513)) - Map data table columns to constructor parameters without having a related property for data table "assist" helpers (e.g. `CreateInstance`). To use this feature you need to set the `InstanceCreationOptions.RequireTableToProvideAllConstructorParameters` flag. ([#​488](https://redirect.github.com/reqnroll/Reqnroll/issues/488)) #### Bug fixes: - Fix: Microsoft.Extensions.DependencyInjection.ReqnrollPlugin, the plugin was only searching for \[ScenarioDependencies] in assemblies with step definitions ([#​477](https://redirect.github.com/reqnroll/Reqnroll/issues/477)) - Fix: xUnit Conservative Mode is not supported together with xUnit v2 ([#​473](https://redirect.github.com/reqnroll/Reqnroll/issues/473)) - Fix: Messages logged through `IReqnrollOutputHelper` are added to the output with a `->` prefix that should be reserved for output messages of Reqnroll itself ([#​504](https://redirect.github.com/reqnroll/Reqnroll/issues/504)) *Contributors of this release (in alphabetical order):* [@​304NotModified](https://redirect.github.com/304NotModified), [@​AroglDarthu](https://redirect.github.com/AroglDarthu), [@​DerAlbertCom](https://redirect.github.com/DerAlbertCom), [@​gasparnagy](https://redirect.github.com/gasparnagy), [@​obligaron](https://redirect.github.com/obligaron), [@​Socolin](https://redirect.github.com/Socolin)
--- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c445681f..5f5b23d0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + From 074de3d2e92560359b82b8dcc83f01015184eb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 7 Mar 2025 18:45:45 +0000 Subject: [PATCH 270/316] test: Update Evaluation Definitions (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request introduces several changes to the `OpenFeature.E2ETests` project, focusing on enhancing configuration and refactoring the step definitions for better state management and code clarity. The most important changes include updates to the VS Code configuration files, refactoring of the `BaseStepDefinitions` and `EvaluationStepDefinitions` classes, and enhancements to the feature flag evaluation logic. Configuration updates: * [`.vscode/extensions.json`](diffhunk://#diff-c16655a98a3ee89a7636a59c59a72b0e93649e3a1e947327cfc43a1336b4e912R1-R6): Added recommendations for the `cucumberopen.cucumber-official` and `ms-dotnettools.csdevkit` extensions. * [`.vscode/settings.json`](diffhunk://#diff-a5de3e5871ffcc383a2294845bd3df25d3eeff6c29ad46e3a396577c413bf357R1-R22): Enabled file nesting, added patterns for feature files, and excluded compilation results and other unnecessary files. Refactoring step definitions: * [`test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs`](diffhunk://#diff-d602b805929d1f2c0a4e819949274cd996296363676f8f59efd1b2997845f46aL13-R80): Refactored to use a shared `State` class for managing feature flag state and evaluation results, replacing internal variables with properties of the `State` class. * [`test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs`](diffhunk://#diff-9ca6e89533e4b3f7a2deaf8de6d6f07a80b7eab2afa6f2e8bfc682b9ca60dc6bL1-R167): Refactored to inherit from `BaseStepDefinitions`, using the shared `State` class for managing state and results, and converting synchronous methods to asynchronous. Enhancements to feature flag evaluation: * [`test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs`](diffhunk://#diff-d602b805929d1f2c0a4e819949274cd996296363676f8f59efd1b2997845f46aL13-R80): Improved the flag evaluation logic by encapsulating flag properties in a `FlagState` class and updating the evaluation methods to use this new structure. * [`test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs`](diffhunk://#diff-d602b805929d1f2c0a4e819949274cd996296363676f8f59efd1b2997845f46aR104-R158): Added new flag variants and context-aware flag evaluation logic to handle more complex scenarios. ### Related Issues Fixes #391 --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .vscode/extensions.json | 6 + .vscode/settings.json | 22 + .../Steps/BaseStepDefinitions.cs | 104 +++- .../Steps/EvaluationStepDefinitions.cs | 586 ++++++++---------- .../Steps/HooksStepDefinitions.cs | 16 +- .../Steps/MetadataStepDefinitions.cs | 32 +- .../Utils/DataTableRows.cs | 16 + test/OpenFeature.E2ETests/Utils/FlagState.cs | 15 + .../Utils/FlagTypesUtil.cs | 5 +- test/OpenFeature.E2ETests/Utils/State.cs | 13 + test/OpenFeature.E2ETests/Utils/TestHook.cs | 2 +- 11 files changed, 418 insertions(+), 399 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 test/OpenFeature.E2ETests/Utils/DataTableRows.cs create mode 100644 test/OpenFeature.E2ETests/Utils/FlagState.cs create mode 100644 test/OpenFeature.E2ETests/Utils/State.cs diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..45138dd7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "cucumberopen.cucumber-official", + "ms-dotnettools.csdevkit" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..a5d74da8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + // shows *.feature.cs files as nested items + "*.feature": "${capture}.feature.cs" + }, + "files.exclude": { + // excludes compilation result + "**/obj/": true, + "**/bin/": true, + "BenchmarkDotNet.Artifacts/": true, + ".idea/": true + }, + "cucumber.glue": [ + // sets the location of the step definition classes + "test/OpenFeature.E2ETests/Steps/*.cs" + ], + "cucumber.features": [ + // sets the location of the feature files + "test/OpenFeature.E2ETests/Features/*.feature" + ] +} diff --git a/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs index c19a5a59..1e8311ae 100644 --- a/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs @@ -10,75 +10,74 @@ namespace OpenFeature.E2ETests.Steps; [Binding] public class BaseStepDefinitions { - internal FeatureClient? Client; - private string _flagKey = null!; - private string _defaultValue = null!; - internal FlagType? FlagTypeEnum; - internal object Result = null!; + protected readonly State State; + + public BaseStepDefinitions(State state) + { + this.State = state; + } [Given(@"a stable provider")] public void GivenAStableProvider() { var memProvider = new InMemoryProvider(E2EFlagConfig); Api.Instance.SetProviderAsync(memProvider).Wait(); - this.Client = Api.Instance.GetClient("TestClient", "1.0.0"); + this.State.Client = Api.Instance.GetClient("TestClient", "1.0.0"); } [Given(@"a Boolean-flag with key ""(.*)"" and a default value ""(.*)""")] [Given(@"a boolean-flag with key ""(.*)"" and a default value ""(.*)""")] public void GivenABoolean_FlagWithKeyAndADefaultValue(string key, string defaultType) { - this._flagKey = key; - this._defaultValue = defaultType; - this.FlagTypeEnum = FlagType.Boolean; + var flagState = new FlagState(key, defaultType, FlagType.Boolean); + this.State.Flag = flagState; } [Given(@"a Float-flag with key ""(.*)"" and a default value ""(.*)""")] [Given(@"a float-flag with key ""(.*)"" and a default value ""(.*)""")] public void GivenAFloat_FlagWithKeyAndADefaultValue(string key, string defaultType) { - this._flagKey = key; - this._defaultValue = defaultType; - this.FlagTypeEnum = FlagType.Float; + var flagState = new FlagState(key, defaultType, FlagType.Float); + this.State.Flag = flagState; } [Given(@"a Integer-flag with key ""(.*)"" and a default value ""(.*)""")] [Given(@"a integer-flag with key ""(.*)"" and a default value ""(.*)""")] public void GivenAnInteger_FlagWithKeyAndADefaultValue(string key, string defaultType) { - this._flagKey = key; - this._defaultValue = defaultType; - this.FlagTypeEnum = FlagType.Integer; + var flagState = new FlagState(key, defaultType, FlagType.Integer); + this.State.Flag = flagState; } [Given(@"a String-flag with key ""(.*)"" and a default value ""(.*)""")] [Given(@"a string-flag with key ""(.*)"" and a default value ""(.*)""")] public void GivenAString_FlagWithKeyAndADefaultValue(string key, string defaultType) { - this._flagKey = key; - this._defaultValue = defaultType; - this.FlagTypeEnum = FlagType.String; + var flagState = new FlagState(key, defaultType, FlagType.String); + this.State.Flag = flagState; } [When(@"the flag was evaluated with details")] public async Task WhenTheFlagWasEvaluatedWithDetails() { - switch (this.FlagTypeEnum) + var flag = this.State.Flag!; + + switch (flag.Type) { case FlagType.Boolean: - this.Result = await this.Client! - .GetBooleanDetailsAsync(this._flagKey, bool.Parse(this._defaultValue)).ConfigureAwait(false); + this.State.FlagEvaluationDetailsResult = await this.State.Client! + .GetBooleanDetailsAsync(flag.Key, bool.Parse(flag.DefaultValue)).ConfigureAwait(false); break; case FlagType.Float: - this.Result = await this.Client! - .GetDoubleDetailsAsync(this._flagKey, double.Parse(this._defaultValue)).ConfigureAwait(false); + this.State.FlagEvaluationDetailsResult = await this.State.Client! + .GetDoubleDetailsAsync(flag.Key, double.Parse(flag.DefaultValue)).ConfigureAwait(false); break; case FlagType.Integer: - this.Result = await this.Client! - .GetIntegerDetailsAsync(this._flagKey, int.Parse(this._defaultValue)).ConfigureAwait(false); + this.State.FlagEvaluationDetailsResult = await this.State.Client! + .GetIntegerDetailsAsync(flag.Key, int.Parse(flag.DefaultValue)).ConfigureAwait(false); break; case FlagType.String: - this.Result = await this.Client!.GetStringDetailsAsync(this._flagKey, this._defaultValue) + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetStringDetailsAsync(flag.Key, flag.DefaultValue) .ConfigureAwait(false); break; } @@ -102,22 +101,61 @@ public async Task WhenTheFlagWasEvaluatedWithDetails() defaultVariant: "on" ) }, + { + "string-flag", new Flag( + variants: new Dictionary() { { "greeting", "hi" }, { "parting", "bye" } }, + defaultVariant: "greeting" + ) + }, { "integer-flag", new Flag( - variants: new Dictionary { { "23", 23 }, { "42", 42 } }, - defaultVariant: "23" + variants: new Dictionary() { { "one", 1 }, { "ten", 10 } }, + defaultVariant: "ten" ) }, { "float-flag", new Flag( - variants: new Dictionary { { "2.3", 2.3 }, { "4.2", 4.2 } }, - defaultVariant: "2.3" + variants: new Dictionary() { { "tenth", 0.1 }, { "half", 0.5 } }, + defaultVariant: "half" ) }, { - "string-flag", new Flag( - variants: new Dictionary { { "value", "value" }, { "value2", "value2" } }, - defaultVariant: "value" + "object-flag", new Flag( + variants: new Dictionary() + { + { "empty", new Value() }, + { + "template", new Value(Structure.Builder() + .Set("showImages", true) + .Set("title", "Check out these pics!") + .Set("imagesPerPage", 100).Build() + ) + } + }, + defaultVariant: "template" + ) + }, + { + "context-aware", new Flag( + variants: new Dictionary() { { "internal", "INTERNAL" }, { "external", "EXTERNAL" } }, + defaultVariant: "external", + (context) => + { + if (context.GetValue("fn").AsString == "SulisΕ‚aw" + && context.GetValue("ln").AsString == "ŚwiΔ™topeΕ‚k" + && context.GetValue("age").AsInteger == 29 + && context.GetValue("customer").AsBoolean == false) + { + return "internal"; + } + else return "external"; + } + ) + }, + { + "wrong-flag", new Flag( + variants: new Dictionary() { { "one", "uno" }, { "two", "dos" } }, + defaultVariant: "one" ) } }; diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index 2a375d23..6efcf3d4 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -1,345 +1,261 @@ -using System.Collections.Generic; using System.Threading.Tasks; using OpenFeature.Constant; +using OpenFeature.E2ETests.Utils; using OpenFeature.Extension; using OpenFeature.Model; -using OpenFeature.Providers.Memory; using Reqnroll; using Xunit; -namespace OpenFeature.E2ETests.Steps +namespace OpenFeature.E2ETests.Steps; + +[Binding] +[Scope(Feature = "Flag evaluation")] +public class EvaluationStepDefinitions : BaseStepDefinitions { - [Binding] - [Scope(Feature = "Flag evaluation")] - public class EvaluationStepDefinitions - { - private FeatureClient? _client; - private Task? _booleanFlagValue; - private Task? _stringFlagValue; - private Task? _intFlagValue; - private Task? _doubleFlagValue; - private Task? _objectFlagValue; - private Task>? _booleanFlagDetails; - private Task>? _stringFlagDetails; - private Task>? _intFlagDetails; - private Task>? _doubleFlagDetails; - private Task>? _objectFlagDetails; - private string? _contextAwareFlagKey; - private string? _contextAwareDefaultValue; - private string? _contextAwareValue; - private EvaluationContext? _context; - private string? _notFoundFlagKey; - private string? _notFoundDefaultValue; - private FlagEvaluationDetails? _notFoundDetails; - private string? _typeErrorFlagKey; - private int _typeErrorDefaultValue; - private FlagEvaluationDetails? _typeErrorDetails; - - [Given("a stable provider")] - public void GivenAStableProvider() - { - var memProvider = new InMemoryProvider(this._e2EFlagConfig); - Api.Instance.SetProviderAsync(memProvider).Wait(); - this._client = Api.Instance.GetClient("TestClient", "1.0.0"); - } - - [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] - public void Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) - { - this._booleanFlagValue = this._client?.GetBooleanValueAsync(flagKey, defaultValue); - } - - [Then(@"the resolved boolean value should be ""(.*)""")] - public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) - { - Assert.Equal(expectedValue, this._booleanFlagValue?.Result); - } - - [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] - public void Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) - { - this._stringFlagValue = this._client?.GetStringValueAsync(flagKey, defaultValue); - } - - [Then(@"the resolved string value should be ""(.*)""")] - public void Thentheresolvedstringvalueshouldbe(string expected) - { - Assert.Equal(expected, this._stringFlagValue?.Result); - } - - [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] - public void Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) - { - this._intFlagValue = this._client?.GetIntegerValueAsync(flagKey, defaultValue); - } - - [Then(@"the resolved integer value should be (.*)")] - public void Thentheresolvedintegervalueshouldbe(int expected) - { - Assert.Equal(expected, this._intFlagValue?.Result); - } - - [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] - public void Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) - { - this._doubleFlagValue = this._client?.GetDoubleValueAsync(flagKey, defaultValue); - } - - [Then(@"the resolved float value should be (.*)")] - public void Thentheresolvedfloatvalueshouldbe(double expected) - { - Assert.Equal(expected, this._doubleFlagValue?.Result); - } - - [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] - public void Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) - { - this._objectFlagValue = this._client?.GetObjectValueAsync(flagKey, new Value()); - } - - [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] - public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) - { - Value? value = this._objectFlagValue?.Result; - Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); - Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); - Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); - } - - [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] - public void Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) - { - this._booleanFlagDetails = this._client?.GetBooleanDetailsAsync(flagKey, defaultValue); - } - - [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] - public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(bool expectedValue, string expectedVariant, string expectedReason) - { - var result = this._booleanFlagDetails?.Result; - Assert.Equal(expectedValue, result?.Value); - Assert.Equal(expectedVariant, result?.Variant); - Assert.Equal(expectedReason, result?.Reason); - } - - [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] - public void Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) - { - this._stringFlagDetails = this._client?.GetStringDetailsAsync(flagKey, defaultValue); - } - - [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] - public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(string expectedValue, string expectedVariant, string expectedReason) - { - var result = this._stringFlagDetails?.Result; - Assert.Equal(expectedValue, result?.Value); - Assert.Equal(expectedVariant, result?.Variant); - Assert.Equal(expectedReason, result?.Reason); - } - - [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] - public void Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) - { - this._intFlagDetails = this._client?.GetIntegerDetailsAsync(flagKey, defaultValue); - } - - [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] - public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(int expectedValue, string expectedVariant, string expectedReason) - { - var result = this._intFlagDetails?.Result; - Assert.Equal(expectedValue, result?.Value); - Assert.Equal(expectedVariant, result?.Variant); - Assert.Equal(expectedReason, result?.Reason); - } - - [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] - public void Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) - { - this._doubleFlagDetails = this._client?.GetDoubleDetailsAsync(flagKey, defaultValue); - } - - [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] - public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(double expectedValue, string expectedVariant, string expectedReason) - { - var result = this._doubleFlagDetails?.Result; - Assert.Equal(expectedValue, result?.Value); - Assert.Equal(expectedVariant, result?.Variant); - Assert.Equal(expectedReason, result?.Reason); - } - - [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] - public void Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) - { - this._objectFlagDetails = this._client?.GetObjectDetailsAsync(flagKey, new Value()); - } - - [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] - public void Thentheresolvedobjectdetailsvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) - { - var value = this._objectFlagDetails?.Result.Value; - Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); - Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); - Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); - } - - [Then(@"the variant should be ""(.*)"", and the reason should be ""(.*)""")] - public void Giventhevariantshouldbeandthereasonshouldbe(string expectedVariant, string expectedReason) - { - Assert.Equal(expectedVariant, this._objectFlagDetails?.Result.Variant); - Assert.Equal(expectedReason, this._objectFlagDetails?.Result.Reason); - } - - [When(@"context contains keys ""(.*)"", ""(.*)"", ""(.*)"", ""(.*)"" with values ""(.*)"", ""(.*)"", (.*), ""(.*)""")] - public void Whencontextcontainskeyswithvalues(string field1, string field2, string field3, string field4, string value1, string value2, int value3, string value4) - { - this._context = new EvaluationContextBuilder() - .Set(field1, value1) - .Set(field2, value2) - .Set(field3, value3) - .Set(field4, bool.Parse(value4)).Build(); - } - - [When(@"a flag with key ""(.*)"" is evaluated with default value ""(.*)""")] - public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) - { - this._contextAwareFlagKey = flagKey; - this._contextAwareDefaultValue = defaultValue; - this._contextAwareValue = this._client?.GetStringValueAsync(flagKey, this._contextAwareDefaultValue, this._context).Result; - } - - [Then(@"the resolved string response should be ""(.*)""")] - public void Thentheresolvedstringresponseshouldbe(string expected) - { - Assert.Equal(expected, this._contextAwareValue); - } - - [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] - public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) - { - string? emptyContextValue = this._client?.GetStringValueAsync(this._contextAwareFlagKey!, this._contextAwareDefaultValue!, EvaluationContext.Empty).Result; - Assert.Equal(expected, emptyContextValue); - } - - [When(@"a non-existent string flag with key ""(.*)"" is evaluated with details and a default value ""(.*)""")] - public void Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultvalue(string flagKey, string defaultValue) - { - this._notFoundFlagKey = flagKey; - this._notFoundDefaultValue = defaultValue; - this._notFoundDetails = this._client?.GetStringDetailsAsync(this._notFoundFlagKey, this._notFoundDefaultValue).Result; - } - - [Then(@"the default string value should be returned")] - public void Thenthedefaultstringvalueshouldbereturned() - { - Assert.Equal(this._notFoundDefaultValue, this._notFoundDetails?.Value); - } - - [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] - public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) - { - Assert.Equal(Reason.Error, this._notFoundDetails?.Reason); - Assert.Equal(errorCode, this._notFoundDetails?.ErrorType.GetDescription()); - } - - [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] - public void Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultvalue(string flagKey, int defaultValue) - { - this._typeErrorFlagKey = flagKey; - this._typeErrorDefaultValue = defaultValue; - this._typeErrorDetails = this._client?.GetIntegerDetailsAsync(this._typeErrorFlagKey, this._typeErrorDefaultValue).Result; - } - - [Then(@"the default integer value should be returned")] - public void Thenthedefaultintegervalueshouldbereturned() - { - Assert.Equal(this._typeErrorDefaultValue, this._typeErrorDetails?.Value); - } - - [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] - public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) - { - Assert.Equal(Reason.Error, this._typeErrorDetails?.Reason); - Assert.Equal(errorCode, this._typeErrorDetails?.ErrorType.GetDescription()); - } - - private readonly IDictionary _e2EFlagConfig = new Dictionary(){ - { - "boolean-flag", new Flag( - variants: new Dictionary(){ - { "on", true }, - { "off", false } - }, - defaultVariant: "on" - ) - }, - { - "string-flag", new Flag( - variants: new Dictionary(){ - { "greeting", "hi" }, - { "parting", "bye" } - }, - defaultVariant: "greeting" - ) - }, - { - "integer-flag", new Flag( - variants: new Dictionary(){ - { "one", 1 }, - { "ten", 10 } - }, - defaultVariant: "ten" - ) - }, - { - "float-flag", new Flag( - variants: new Dictionary(){ - { "tenth", 0.1 }, - { "half", 0.5 } - }, - defaultVariant: "half" - ) - }, - { - "object-flag", new Flag( - variants: new Dictionary(){ - { "empty", new Value() }, - { "template", new Value(Structure.Builder() - .Set("showImages", true) - .Set("title", "Check out these pics!") - .Set("imagesPerPage", 100).Build() - ) - } - }, - defaultVariant: "template" - ) - }, - { - "context-aware", new Flag( - variants: new Dictionary(){ - { "internal", "INTERNAL" }, - { "external", "EXTERNAL" } - }, - defaultVariant: "external", - (context) => { - if (context.GetValue("fn").AsString == "SulisΕ‚aw" - && context.GetValue("ln").AsString == "ŚwiΔ™topeΕ‚k" - && context.GetValue("age").AsInteger == 29 - && context.GetValue("customer").AsBoolean == false) - { - return "internal"; - } - else return "external"; - } - ) - }, - { - "wrong-flag", new Flag( - variants: new Dictionary(){ - { "one", "uno" }, - { "two", "dos" } - }, - defaultVariant: "one" - ) - } - }; + public EvaluationStepDefinitions(State state) : base(state) + { + } + + [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] + public async Task Whenabooleanflagwithkeyisevaluatedwithdefaultvalue(string flagKey, bool defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue.ToString(), FlagType.Boolean); + this.State.FlagResult = await this.State.Client!.GetBooleanValueAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved boolean value should be ""(.*)""")] + public void Thentheresolvedbooleanvalueshouldbe(bool expectedValue) + { + var result = this.State.FlagResult as bool?; + Assert.Equal(expectedValue, result); + } + + [When(@"a string flag with key ""(.*)"" is evaluated with default value ""(.*)""")] + public async Task Whenastringflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue, FlagType.String); + this.State.FlagResult = await this.State.Client!.GetStringValueAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved string value should be ""(.*)""")] + public void Thentheresolvedstringvalueshouldbe(string expected) + { + var result = this.State.FlagResult as string; + Assert.Equal(expected, result); + } + + [When(@"an integer flag with key ""(.*)"" is evaluated with default value (.*)")] + public async Task Whenanintegerflagwithkeyisevaluatedwithdefaultvalue(string flagKey, int defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue.ToString(), FlagType.Integer); + this.State.FlagResult = await this.State.Client!.GetIntegerValueAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved integer value should be (.*)")] + public void Thentheresolvedintegervalueshouldbe(int expected) + { + var result = this.State.FlagResult as int?; + Assert.Equal(expected, result); + } + + [When(@"a float flag with key ""(.*)"" is evaluated with default value (.*)")] + public async Task Whenafloatflagwithkeyisevaluatedwithdefaultvalue(string flagKey, double defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue.ToString(), FlagType.Float); + this.State.FlagResult = await this.State.Client!.GetDoubleValueAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved float value should be (.*)")] + public void Thentheresolvedfloatvalueshouldbe(double expected) + { + var result = this.State.FlagResult as double?; + Assert.Equal(expected, result); + } + + [When(@"an object flag with key ""(.*)"" is evaluated with a null default value")] + public async Task Whenanobjectflagwithkeyisevaluatedwithanulldefaultvalue(string flagKey) + { + this.State.Flag = new FlagState(flagKey, null!, FlagType.Object); + this.State.FlagResult = await this.State.Client!.GetObjectValueAsync(flagKey, new Value()).ConfigureAwait(false); + } + + [Then(@"the resolved object value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] + public void Thentheresolvedobjectvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) + { + Value? value = this.State.FlagResult as Value; + Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); + Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); + Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); + } + + [When(@"a boolean flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] + public async Task Whenabooleanflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, bool defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue.ToString(), FlagType.Boolean); + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetBooleanDetailsAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved boolean details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedbooleandetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(bool expectedValue, string expectedVariant, string expectedReason) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); + } + + [When(@"a string flag with key ""(.*)"" is evaluated with details and default value ""(.*)""")] + public async Task Whenastringflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, string defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue, FlagType.String); + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetStringDetailsAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved string details value should be ""(.*)"", the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedstringdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(string expectedValue, string expectedVariant, string expectedReason) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); + } + + [When(@"an integer flag with key ""(.*)"" is evaluated with details and default value (.*)")] + public async Task Whenanintegerflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, int defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue.ToString(), FlagType.Integer); + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetIntegerDetailsAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved integer details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedintegerdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(int expectedValue, string expectedVariant, string expectedReason) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); + } + + [When(@"a float flag with key ""(.*)"" is evaluated with details and default value (.*)")] + public async Task Whenafloatflagwithkeyisevaluatedwithdetailsanddefaultvalue(string flagKey, double defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue.ToString(), FlagType.Float); + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetDoubleDetailsAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the resolved float details value should be (.*), the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Thentheresolvedfloatdetailsvalueshouldbethevariantshouldbeandthereasonshouldbe(double expectedValue, string expectedVariant, string expectedReason) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + Assert.Equal(expectedValue, result?.Value); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); + } + + [When(@"an object flag with key ""(.*)"" is evaluated with details and a null default value")] + public async Task Whenanobjectflagwithkeyisevaluatedwithdetailsandanulldefaultvalue(string flagKey) + { + this.State.Flag = new FlagState(flagKey, null!, FlagType.Object); + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetObjectDetailsAsync(flagKey, new Value()).ConfigureAwait(false); + } + + [Then(@"the resolved object details value should be contain fields ""(.*)"", ""(.*)"", and ""(.*)"", with values ""(.*)"", ""(.*)"" and (.*), respectively")] + public void Thentheresolvedobjectdetailsvalueshouldbecontainfieldsandwithvaluesandrespectively(string boolField, string stringField, string numberField, bool boolValue, string stringValue, int numberValue) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + var value = result?.Value; + Assert.NotNull(value); + Assert.Equal(boolValue, value?.AsStructure?[boolField].AsBoolean); + Assert.Equal(stringValue, value?.AsStructure?[stringField].AsString); + Assert.Equal(numberValue, value?.AsStructure?[numberField].AsInteger); + } + + [Then(@"the variant should be ""(.*)"", and the reason should be ""(.*)""")] + public void Giventhevariantshouldbeandthereasonshouldbe(string expectedVariant, string expectedReason) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + Assert.NotNull(result); + Assert.Equal(expectedVariant, result?.Variant); + Assert.Equal(expectedReason, result?.Reason); + } + + [When(@"context contains keys ""(.*)"", ""(.*)"", ""(.*)"", ""(.*)"" with values ""(.*)"", ""(.*)"", (.*), ""(.*)""")] + public void Whencontextcontainskeyswithvalues(string field1, string field2, string field3, string field4, string value1, string value2, int value3, string value4) + { + this.State.EvaluationContext = new EvaluationContextBuilder() + .Set(field1, value1) + .Set(field2, value2) + .Set(field3, value3) + .Set(field4, bool.Parse(value4)).Build(); + } + + [When(@"a flag with key ""(.*)"" is evaluated with default value ""(.*)""")] + public async Task Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue, FlagType.String); + this.State.FlagResult = await this.State.Client!.GetStringValueAsync(flagKey, defaultValue, this.State.EvaluationContext).ConfigureAwait(false); + } + + [Then(@"the resolved string response should be ""(.*)""")] + public void Thentheresolvedstringresponseshouldbe(string expected) + { + var result = this.State.FlagResult as string; + Assert.Equal(expected, result); + } + + [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] + public async Task Giventheresolvedflagvalueiswhenthecontextisempty(string expected) + { + var key = this.State.Flag!.Key; + var defaultValue = this.State.Flag.DefaultValue; + + string? emptyContextValue = await this.State.Client!.GetStringValueAsync(key, defaultValue, EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal(expected, emptyContextValue); + } + + [When(@"a non-existent string flag with key ""(.*)"" is evaluated with details and a default value ""(.*)""")] + public async Task Whenanonexistentstringflagwithkeyisevaluatedwithdetailsandadefaultvalue(string flagKey, string defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue, FlagType.String); + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetStringDetailsAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the default string value should be returned")] + public void Thenthedefaultstringvalueshouldbereturned() + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + var defaultValue = this.State.Flag!.DefaultValue; + Assert.Equal(defaultValue, result?.Value); + } + + [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] + public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + Assert.Equal(Reason.Error, result?.Reason); + Assert.Equal(errorCode, result?.ErrorType.GetDescription()); + } + + [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] + public async Task Whenastringflagwithkeyisevaluatedasanintegerwithdetailsandadefaultvalue(string flagKey, int defaultValue) + { + this.State.Flag = new FlagState(flagKey, defaultValue.ToString(), FlagType.String); + this.State.FlagEvaluationDetailsResult = await this.State.Client!.GetIntegerDetailsAsync(flagKey, defaultValue).ConfigureAwait(false); + } + + [Then(@"the default integer value should be returned")] + public void Thenthedefaultintegervalueshouldbereturned() + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + var defaultValue = int.Parse(this.State.Flag!.DefaultValue); + Assert.Equal(defaultValue, result?.Value); + } + + [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] + public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) + { + var result = this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails; + Assert.Equal(Reason.Error, result?.Reason); + Assert.Equal(errorCode, result?.ErrorType.GetDescription()); } } diff --git a/test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs index 32e85679..c8882baa 100644 --- a/test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/HooksStepDefinitions.cs @@ -8,13 +8,15 @@ namespace OpenFeature.E2ETests.Steps; [Scope(Feature = "Evaluation details through hooks")] public class HooksStepDefinitions : BaseStepDefinitions { - private TestHook? _testHook; + public HooksStepDefinitions(State state) : base(state) + { + } [Given(@"a client with added hook")] public void GivenAClientWithAddedHook() { - this._testHook = new TestHook(); - this.Client!.AddHooks(this._testHook); + this.State.TestHook = new TestHook(); + this.State.Client!.AddHooks(this.State.TestHook); } [Then(@"the ""(.*)"" hook should have been executed")] @@ -116,16 +118,16 @@ private void CheckHookExecution(string hook) switch (hook) { case "before": - Assert.Equal(1, this._testHook!.BeforeCount); + Assert.Equal(1, this.State.TestHook!.BeforeCount); break; case "after": - Assert.Equal(1, this._testHook!.AfterCount); + Assert.Equal(1, this.State.TestHook!.AfterCount); break; case "error": - Assert.Equal(1, this._testHook!.ErrorCount); + Assert.Equal(1, this.State.TestHook!.ErrorCount); break; case "finally": - Assert.Equal(1, this._testHook!.FinallyCount); + Assert.Equal(1, this.State.TestHook!.FinallyCount); break; } } diff --git a/test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs index 7b90990b..63c8cdbe 100644 --- a/test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/MetadataStepDefinitions.cs @@ -11,12 +11,16 @@ namespace OpenFeature.E2ETests.Steps; [Scope(Feature = "Metadata")] public class MetadataStepDefinitions : BaseStepDefinitions { + MetadataStepDefinitions(State state) : base(state) + { + } + [Then("the resolved metadata should contain")] [Scope(Scenario = "Returns metadata")] public void ThenTheResolvedMetadataShouldContain(DataTable itemsTable) { var items = itemsTable.Rows.Select(row => new DataTableRows(row["key"], row["value"], row["metadata_type"])).ToList(); - var metadata = (this.Result as FlagEvaluationDetails)?.FlagMetadata; + var metadata = (this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails)?.FlagMetadata; foreach (var item in items) { @@ -48,37 +52,23 @@ public void ThenTheResolvedMetadataShouldContain(DataTable itemsTable) [Then("the resolved metadata is empty")] public void ThenTheResolvedMetadataIsEmpty() { - switch (this.FlagTypeEnum) + var flag = this.State.Flag!; + switch (flag.Type) { case FlagType.Boolean: - Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + Assert.Null((this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails)?.FlagMetadata?.Count); break; case FlagType.Float: - Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + Assert.Null((this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails)?.FlagMetadata?.Count); break; case FlagType.Integer: - Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + Assert.Null((this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails)?.FlagMetadata?.Count); break; case FlagType.String: - Assert.Null((this.Result as FlagEvaluationDetails)?.FlagMetadata?.Count); + Assert.Null((this.State.FlagEvaluationDetailsResult as FlagEvaluationDetails)?.FlagMetadata?.Count); break; default: throw new ArgumentOutOfRangeException(); } } - - private class DataTableRows - { - public DataTableRows(string key, string value, string metadataType) - { - this.Key = key; - this.Value = value; - - this.MetadataType = FlagTypesUtil.ToEnum(metadataType); - } - - public string Key { get; } - public string Value { get; } - public FlagType MetadataType { get; } - } } diff --git a/test/OpenFeature.E2ETests/Utils/DataTableRows.cs b/test/OpenFeature.E2ETests/Utils/DataTableRows.cs new file mode 100644 index 00000000..45e43cc5 --- /dev/null +++ b/test/OpenFeature.E2ETests/Utils/DataTableRows.cs @@ -0,0 +1,16 @@ +using OpenFeature.E2ETests.Utils; + +internal class DataTableRows +{ + public DataTableRows(string key, string value, string metadataType) + { + this.Key = key; + this.Value = value; + + this.MetadataType = FlagTypesUtil.ToEnum(metadataType); + } + + public string Key { get; } + public string Value { get; } + public FlagType MetadataType { get; } +} diff --git a/test/OpenFeature.E2ETests/Utils/FlagState.cs b/test/OpenFeature.E2ETests/Utils/FlagState.cs new file mode 100644 index 00000000..375ab55d --- /dev/null +++ b/test/OpenFeature.E2ETests/Utils/FlagState.cs @@ -0,0 +1,15 @@ +namespace OpenFeature.E2ETests.Utils; + +public class FlagState +{ + public FlagState(string key, string defaultValue, FlagType type) + { + this.Key = key; + this.DefaultValue = defaultValue; + this.Type = type; + } + + public string Key { get; private set; } + public string DefaultValue { get; private set; } + public FlagType Type { get; private set; } +} diff --git a/test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs b/test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs index aa5c91dd..5b05c799 100644 --- a/test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs +++ b/test/OpenFeature.E2ETests/Utils/FlagTypesUtil.cs @@ -19,10 +19,11 @@ internal static FlagType ToEnum(string flagType) } } -internal enum FlagType +public enum FlagType { Integer, Float, String, - Boolean + Boolean, + Object } diff --git a/test/OpenFeature.E2ETests/Utils/State.cs b/test/OpenFeature.E2ETests/Utils/State.cs new file mode 100644 index 00000000..b3380132 --- /dev/null +++ b/test/OpenFeature.E2ETests/Utils/State.cs @@ -0,0 +1,13 @@ +using OpenFeature.Model; + +namespace OpenFeature.E2ETests.Utils; + +public class State +{ + public FeatureClient? Client; + public FlagState? Flag; + public object? FlagEvaluationDetailsResult; + public TestHook? TestHook; + public object? FlagResult; + public EvaluationContext? EvaluationContext; +} diff --git a/test/OpenFeature.E2ETests/Utils/TestHook.cs b/test/OpenFeature.E2ETests/Utils/TestHook.cs index 7fd204f5..fbe7568b 100644 --- a/test/OpenFeature.E2ETests/Utils/TestHook.cs +++ b/test/OpenFeature.E2ETests/Utils/TestHook.cs @@ -8,7 +8,7 @@ namespace OpenFeature.E2ETests.Utils; [ExcludeFromCodeCoverage] -internal class TestHook : Hook +public class TestHook : Hook { private int _afterCount; private int _beforeCount; From 9b6feab50085ee7dfcca190fe42f583c072ae50d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 19:56:39 +0000 Subject: [PATCH 271/316] chore(deps): update github/codeql-action digest to 6bb031a (#398) 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://redirect.github.com/github/codeql-action) | action | digest | `b56ba49` -> `6bb031a` | --- ### 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 dcfa0d60..94a8f0a2 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@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # 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@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # 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@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 From 46274a21d74b5cfffd4cfbc30e5e49e2dc1f256c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:22:30 +0000 Subject: [PATCH 272/316] refactor: Improve EventExecutor (#393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request to `src/OpenFeature/EventExecutor.cs` includes changes to improve code readability and performance. The most important changes include replacing `List` and `Dictionary` initializations with shorthand syntax, switching from `Thread` to `Task` for asynchronous operations, and refactoring methods for better clarity and maintainability. Improvements to code readability and performance: * [`src/OpenFeature/EventExecutor.cs`](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL13-R28): Replaced `List` and `Dictionary` initializations with shorthand syntax `[]`. [[1]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL13-R28) [[2]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL45-R41) [[3]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL73-R75) * [`src/OpenFeature/EventExecutor.cs`](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL149-R144): Changed from using `Thread` to `Task` for asynchronous operations to improve performance and simplify the code. [[1]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL149-R144) [[2]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL219-R213) [[3]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL237-R257) Refactoring for better clarity and maintainability: * [`src/OpenFeature/EventExecutor.cs`](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL189-L204): Refactored `EmitOnRegistration` method to use a switch expression for setting the message based on provider status and event type, improving readability. * [`src/OpenFeature/EventExecutor.cs`](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cR266-R275): Split `ProcessEventAsync` method into smaller methods (`ProcessApiHandlers`, `ProcessClientHandlers`, `ProcessDefaultProviderHandlers`) for better organization and maintainability. [[1]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cR266-R275) [[2]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL281-R298) [[3]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL305-R311) * [`src/OpenFeature/EventExecutor.cs`](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL219-R213): Converted `ProcessFeatureProviderEventsAsync` and `ProcessEventAsync` from `void` to `Task` to follow best practices for asynchronous methods. [[1]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL219-R213) [[2]](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL237-R257) ### Related Issues Reference #358 --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/EventExecutor.cs | 201 +++++++++--------- .../Providers/Memory/InMemoryProvider.cs | 1 + test/OpenFeature.Tests/TestImplementations.cs | 5 - 3 files changed, 101 insertions(+), 106 deletions(-) diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index ad53a949..b54bd9c0 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -10,26 +9,23 @@ namespace OpenFeature { - internal delegate Task ShutdownDelegate(CancellationToken cancellationToken); - internal sealed partial class EventExecutor : IAsyncDisposable { - private readonly object _lockObj = new object(); + private readonly object _lockObj = new(); public readonly Channel EventChannel = Channel.CreateBounded(1); private FeatureProvider? _defaultProvider; - private readonly Dictionary _namedProviderReferences = new Dictionary(); - private readonly List _activeSubscriptions = new List(); + private readonly Dictionary _namedProviderReferences = []; + private readonly List _activeSubscriptions = []; - private readonly Dictionary> _apiHandlers = new Dictionary>(); - private readonly Dictionary>> _clientHandlers = new Dictionary>>(); + private readonly Dictionary> _apiHandlers = []; + private readonly Dictionary>> _clientHandlers = []; private ILogger _logger; public EventExecutor() { this._logger = NullLogger.Instance; - var eventProcessing = new Thread(this.ProcessEventAsync); - eventProcessing.Start(); + Task.Run(this.ProcessEventAsync); } public ValueTask DisposeAsync() => new(this.ShutdownAsync()); @@ -42,7 +38,7 @@ internal void AddApiLevelHandler(ProviderEventTypes eventType, EventHandlerDeleg { if (!this._apiHandlers.TryGetValue(eventType, out var eventHandlers)) { - eventHandlers = new List(); + eventHandlers = []; this._apiHandlers[eventType] = eventHandlers; } @@ -70,13 +66,13 @@ internal void AddClientHandler(string client, ProviderEventTypes eventType, Even // 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 = new Dictionary>(); + registry = []; this._clientHandlers[client] = registry; } if (!this._clientHandlers[client].TryGetValue(eventType, out var eventHandlers)) { - eventHandlers = new List(); + eventHandlers = []; this._clientHandlers[client][eventType] = eventHandlers; } @@ -127,16 +123,15 @@ internal void RegisterClientFeatureProvider(string client, FeatureProvider? prov } lock (this._lockObj) { - var newProvider = provider; FeatureProvider? oldProvider = null; if (this._namedProviderReferences.TryGetValue(client, out var foundOldProvider)) { oldProvider = foundOldProvider; } - this._namedProviderReferences[client] = newProvider; + this._namedProviderReferences[client] = provider; - this.StartListeningAndShutdownOld(newProvider, oldProvider); + this.StartListeningAndShutdownOld(provider, oldProvider); } } @@ -146,8 +141,7 @@ private void StartListeningAndShutdownOld(FeatureProvider newProvider, FeaturePr if (!this.IsProviderActive(newProvider)) { this._activeSubscriptions.Add(newProvider); - var featureProviderEventProcessing = new Thread(this.ProcessFeatureProviderEventsAsync); - featureProviderEventProcessing.Start(newProvider); + Task.Run(() => this.ProcessFeatureProviderEventsAsync(newProvider)); } if (oldProvider != null && !this.IsProviderBound(oldProvider)) @@ -186,42 +180,37 @@ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes ev } var status = provider.Status; - var message = ""; - if (status == ProviderStatus.Ready && eventType == ProviderEventTypes.ProviderReady) - { - message = "Provider is ready"; - } - else if (status == ProviderStatus.Error && eventType == ProviderEventTypes.ProviderError) + var message = status switch { - message = "Provider is in error state"; - } - else if (status == ProviderStatus.Stale && eventType == ProviderEventTypes.ProviderStale) + 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)) { - message = "Provider is in stale state"; + return; } - if (message != "") + try { - try + handler.Invoke(new ProviderEventPayload { - handler.Invoke(new ProviderEventPayload - { - ProviderName = provider.GetMetadata()?.Name, - Type = eventType, - Message = message - }); - } - catch (Exception exc) - { - this.ErrorRunningHandler(exc); - } + ProviderName = provider.GetMetadata()?.Name, + Type = eventType, + Message = message + }); + } + catch (Exception exc) + { + this.ErrorRunningHandler(exc); } } - private async void ProcessFeatureProviderEventsAsync(object? providerRef) + private async Task ProcessFeatureProviderEventsAsync(FeatureProvider provider) { - var typedProviderRef = (FeatureProvider?)providerRef; - if (typedProviderRef?.GetEventChannel() is not { Reader: { } reader }) + if (provider.GetEventChannel() is not { Reader: { } reader }) { return; } @@ -234,82 +223,92 @@ private async void ProcessFeatureProviderEventsAsync(object? providerRef) switch (item) { case ProviderEventPayload eventPayload: - this.UpdateProviderStatus(typedProviderRef, eventPayload); - await this.EventChannel.Writer.WriteAsync(new Event { Provider = typedProviderRef, EventPayload = eventPayload }).ConfigureAwait(false); + UpdateProviderStatus(provider, eventPayload); + await this.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false); break; } } } // Method to process events - private async void ProcessEventAsync() + private async Task ProcessEventAsync() { while (await this.EventChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) { if (!this.EventChannel.Reader.TryRead(out var item)) + { continue; + } - switch (item) + if (item is not Event e) { - case Event e: - lock (this._lockObj) - { - if (e.EventPayload?.Type != null && this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers)) - { - foreach (var eventHandler in eventHandlers) - { - this.InvokeEventHandler(eventHandler, e); - } - } - - // look for client handlers and call invoke method there - foreach (var keyAndValue in this._namedProviderReferences) - { - if (keyAndValue.Value == e.Provider && keyAndValue.Key != null) - { - if (this._clientHandlers.TryGetValue(keyAndValue.Key, out var clientRegistry)) - { - if (e.EventPayload?.Type != null && clientRegistry.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) - { - foreach (var eventHandler in clientEventHandlers) - { - this.InvokeEventHandler(eventHandler, e); - } - } - } - } - } - - if (e.Provider != this._defaultProvider) - { - break; - } - // handling the default provider - invoke event handlers for clients which are not bound - // to a particular feature provider - foreach (var keyAndValues in this._clientHandlers) - { - if (this._namedProviderReferences.TryGetValue(keyAndValues.Key, out _)) - { - // if there is an association for the client to a specific feature provider, then continue - continue; - } - if (e.EventPayload?.Type != null && keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) - { - foreach (var eventHandler in clientEventHandlers) - { - this.InvokeEventHandler(eventHandler, e); - } - } - } - } - break; + continue; + } + + lock (this._lockObj) + { + this.ProcessApiHandlers(e); + this.ProcessClientHandlers(e); + this.ProcessDefaultProviderHandlers(e); + } + } + } + + private void ProcessApiHandlers(Event e) + { + if (e.EventPayload?.Type != null && this._apiHandlers.TryGetValue(e.EventPayload.Type, out var eventHandlers)) + { + foreach (var eventHandler in eventHandlers) + { + this.InvokeEventHandler(eventHandler, e); + } + } + } + + private void ProcessClientHandlers(Event e) + { + 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); + } + } + } + } + + private void ProcessDefaultProviderHandlers(Event e) + { + if (e.Provider != this._defaultProvider) + { + return; + } + + foreach (var keyAndValues in this._clientHandlers) + { + if (this._namedProviderReferences.ContainsKey(keyAndValues.Key)) + { + continue; } + if (e.EventPayload?.Type != null && keyAndValues.Value.TryGetValue(e.EventPayload.Type, out var clientEventHandlers)) + { + foreach (var eventHandler in clientEventHandlers) + { + this.InvokeEventHandler(eventHandler, e); + } + } } } + // map events to provider status as per spec: https://openfeature.dev/specification/sections/events/#requirement-535 - private void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload eventPayload) + private static void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload eventPayload) { switch (eventPayload.Type) { diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 3283ea22..4a06dc85 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -65,6 +65,7 @@ public async Task UpdateFlagsAsync(IDictionary? flags = null) FlagsChanged = changed, // emit all Message = "flags changed", }; + await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false); } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index 724278e8..df738efe 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -152,10 +152,5 @@ internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToke { return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name, }, cancellationToken); } - - internal ValueTask SendEventAsync(ProviderEventPayload payload, CancellationToken cancellationToken = default) - { - return this.EventChannel.Writer.WriteAsync(payload, cancellationToken); - } } } From 00a4e4ab2ccb8984cd3ca57bad6d25e688b1cf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:56:46 +0000 Subject: [PATCH 273/316] fix: Remove virtual GetEventChannel from FeatureProvider (#401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes changes to the `EventChannel` handling in the `OpenFeature` library to improve code consistency and reliability. Improvements to `EventChannel` handling: * [`src/OpenFeature/EventExecutor.cs`](diffhunk://#diff-f563baadcf750bb66efaea76d7ec4783320e6efdc45c468effb531c948a2292cL150-R150): Removed unnecessary null-conditional operator when completing the `Writer` of the event channel. * [`src/OpenFeature/FeatureProvider.cs`](diffhunk://#diff-96ebc8fc507d0a19d55b9a5cb57b72a0e8058e09f31ee7d0b39e99b00c5029d8L143-R143): Made the `GetEventChannel` method non-virtual to ensure consistent behavior across different implementations. ### Related Issues Related to #358 --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/EventExecutor.cs | 2 +- src/OpenFeature/FeatureProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index b54bd9c0..a1c1ddbd 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -147,7 +147,7 @@ private void StartListeningAndShutdownOld(FeatureProvider newProvider, FeaturePr if (oldProvider != null && !this.IsProviderBound(oldProvider)) { this._activeSubscriptions.Remove(oldProvider); - oldProvider.GetEventChannel()?.Writer.Complete(); + oldProvider.GetEventChannel().Writer.Complete(); } } diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index de3f2797..6aff0129 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -140,7 +140,7 @@ public virtual Task ShutdownAsync(CancellationToken cancellationToken = default) /// Returns the event channel of the provider. /// /// The event channel of the provider - public virtual Channel GetEventChannel() => this.EventChannel; + 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. From 564ad2db9c22688ed85d6d9fb6fd4805c686e755 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:57:29 +0000 Subject: [PATCH 274/316] chore(deps): update dotnet monorepo (#400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | Type | Update | |---|---|---|---|---|---|---|---| | [Microsoft.AspNetCore.TestHost](https://asp.net/) ([source](https://redirect.github.com/dotnet/aspnetcore)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.AspNetCore.TestHost/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.AspNetCore.TestHost/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.AspNetCore.TestHost/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.AspNetCore.TestHost/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Bcl.AsyncInterfaces](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Bcl.AsyncInterfaces/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.DependencyInjection](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.DependencyInjection/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.DependencyInjection/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.DependencyInjection/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.DependencyInjection/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.DependencyInjection.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.DependencyInjection.Abstractions/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.Diagnostics.Testing](https://dot.net/) ([source](https://redirect.github.com/dotnet/extensions)) | `9.2.0` -> `9.3.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Diagnostics.Testing/9.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Diagnostics.Testing/9.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Diagnostics.Testing/9.2.0/9.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Diagnostics.Testing/9.2.0/9.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | minor | | [Microsoft.Extensions.Hosting.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Hosting.Abstractions/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.Logging.Abstractions](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Logging.Abstractions/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [Microsoft.Extensions.Options](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Extensions.Options/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.Extensions.Options/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.Extensions.Options/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Extensions.Options/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [System.Collections.Immutable](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/System.Collections.Immutable/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/System.Collections.Immutable/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/System.Collections.Immutable/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/System.Collections.Immutable/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [System.Threading.Channels](https://dot.net/) ([source](https://redirect.github.com/dotnet/runtime)) | `9.0.2` -> `9.0.3` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/System.Threading.Channels/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/System.Threading.Channels/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/System.Threading.Channels/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/System.Threading.Channels/9.0.2/9.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | nuget | patch | | [dotnet-sdk](https://redirect.github.com/dotnet/sdk) | `9.0.200` -> `9.0.201` | [![age](https://developer.mend.io/api/mc/badges/age/dotnet-version/dotnet-sdk/9.0.201?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/dotnet-version/dotnet-sdk/9.0.201?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/dotnet-version/dotnet-sdk/9.0.200/9.0.201?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/dotnet-version/dotnet-sdk/9.0.200/9.0.201?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dotnet-sdk | patch | --- ### Release Notes
dotnet/aspnetcore (Microsoft.AspNetCore.TestHost) ### [`v9.0.3`](https://redirect.github.com/dotnet/aspnetcore/releases/tag/v9.0.3): .NET 9.0.3 [Release](https://redirect.github.com/dotnet/core/releases/tag/v9.0.3) ##### What's Changed - Update branding to 9.0.3 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/aspnetcore/pull/60198](https://redirect.github.com/dotnet/aspnetcore/pull/60198) - \[release/9.0] Fix branding by [@​wtgodbe](https://redirect.github.com/wtgodbe) in [https://github.com/dotnet/aspnetcore/pull/60029](https://redirect.github.com/dotnet/aspnetcore/pull/60029) - \[release/9.0] Update to MacOS 15 in Helix by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/60238](https://redirect.github.com/dotnet/aspnetcore/pull/60238) - \[release/9.0] Revert "Revert "Use the latest available jdk"" by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/60229](https://redirect.github.com/dotnet/aspnetcore/pull/60229) - \[release/9.0] Update `HtmlAttributePropertyHelper` to correctly follow the `MetadataUpdateHandlerAttribute` contract by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/59908](https://redirect.github.com/dotnet/aspnetcore/pull/59908) - \[release/9.0] Fix skip condition for java tests by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/60242](https://redirect.github.com/dotnet/aspnetcore/pull/60242) - \[release/9.0] (deps): Bump src/submodules/googletest from `7d76a23` to `e235eb3` by [@​dependabot](https://redirect.github.com/dependabot) in [https://github.com/dotnet/aspnetcore/pull/60151](https://redirect.github.com/dotnet/aspnetcore/pull/60151) - \[release/9.0] Readd DiagnosticSource to KestrelServerImpl by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/60202](https://redirect.github.com/dotnet/aspnetcore/pull/60202) - \[release/9.0] Redis distributed cache: add HybridCache usage signal by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/aspnetcore/pull/59886](https://redirect.github.com/dotnet/aspnetcore/pull/59886) - \[release/9.0] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/aspnetcore/pull/59952](https://redirect.github.com/dotnet/aspnetcore/pull/59952) - \[release/9.0] Update dependencies from dotnet/extensions by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/aspnetcore/pull/59951](https://redirect.github.com/dotnet/aspnetcore/pull/59951) - \[release/9.0] Update remnants of azureedge.net by [@​sebastienros](https://redirect.github.com/sebastienros) in [https://github.com/dotnet/aspnetcore/pull/60263](https://redirect.github.com/dotnet/aspnetcore/pull/60263) - \[release/9.0] Update dependencies from dotnet/extensions by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/aspnetcore/pull/60291](https://redirect.github.com/dotnet/aspnetcore/pull/60291) - \[release/9.0] Centralize on one docker container by [@​wtgodbe](https://redirect.github.com/wtgodbe) in [https://github.com/dotnet/aspnetcore/pull/60298](https://redirect.github.com/dotnet/aspnetcore/pull/60298) - Revert "\[release/9.0] Update remnants of azureedge.net" by [@​wtgodbe](https://redirect.github.com/wtgodbe) in [https://github.com/dotnet/aspnetcore/pull/60323](https://redirect.github.com/dotnet/aspnetcore/pull/60323) - Merging internal commits for release/9.0 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/aspnetcore/pull/60317](https://redirect.github.com/dotnet/aspnetcore/pull/60317) **Full Changelog**: https://github.com/dotnet/aspnetcore/compare/v9.0.2...v9.0.3
dotnet/runtime (Microsoft.Bcl.AsyncInterfaces) ### [`v9.0.3`](https://redirect.github.com/dotnet/runtime/releases/tag/v9.0.3): .NET 9.0.3 [Release](https://redirect.github.com/dotnet/core/releases/tag/v9.0.3) #### What's Changed - \[release/9.0-staging] Fix wrong alias-to for tvos AOT packs in net8 workload manifest by [@​akoeplinger](https://redirect.github.com/akoeplinger) in [https://github.com/dotnet/runtime/pull/110871](https://redirect.github.com/dotnet/runtime/pull/110871) - \[release/9.0] Disable tests targetting http://corefx-net-http11.azurewebsites.net by [@​rzikm](https://redirect.github.com/rzikm) in [https://github.com/dotnet/runtime/pull/111402](https://redirect.github.com/dotnet/runtime/pull/111402) - \[release/9.0-staging] Support generic fields in PersistedAssemblyBuilder by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/110839](https://redirect.github.com/dotnet/runtime/pull/110839) - \[release/9.0-staging] Re-enable skiasharp WBT tests ([#​109232](https://redirect.github.com/dotnet/runtime/issues/109232)) by [@​radekdoulik](https://redirect.github.com/radekdoulik) in [https://github.com/dotnet/runtime/pull/110734](https://redirect.github.com/dotnet/runtime/pull/110734) - \[release/9.0-staging] Backport test fixes related to BinaryFormatter removal by [@​adamsitnik](https://redirect.github.com/adamsitnik) in [https://github.com/dotnet/runtime/pull/111508](https://redirect.github.com/dotnet/runtime/pull/111508) - \[manual] Merge branch 'release/9.0' => 'release/9.0-staging' by [@​carlossanlop](https://redirect.github.com/carlossanlop) in [https://github.com/dotnet/runtime/pull/111565](https://redirect.github.com/dotnet/runtime/pull/111565) - \[release/9.0] \[wasi] Disable build in .NET 9 by [@​maraf](https://redirect.github.com/maraf) in [https://github.com/dotnet/runtime/pull/108877](https://redirect.github.com/dotnet/runtime/pull/108877) - \[release/9.0-staging] \[mono] Disable UnitTest_GVM_TypeLoadException for fullAOT jobs by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111394](https://redirect.github.com/dotnet/runtime/pull/111394) - \[release/9.0-staging] Fix UnsafeAccessor scenario for modopts/modreqs when comparing field sigs. by [@​AaronRobinsonMSFT](https://redirect.github.com/AaronRobinsonMSFT) in [https://github.com/dotnet/runtime/pull/111675](https://redirect.github.com/dotnet/runtime/pull/111675) - \[release/9.0-staging] \[mono] Run runtime-llvm and runtime-ioslike on Mono LLVM PRs by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111739](https://redirect.github.com/dotnet/runtime/pull/111739) - \[release/9.0-staging] fix stack 2x2 tensor along dimension 1 by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/110053](https://redirect.github.com/dotnet/runtime/pull/110053) - \[release/9.0-staging] Fix race condition in cleanup of collectible thread static variables by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111275](https://redirect.github.com/dotnet/runtime/pull/111275) - \[release/9.0-staging] \[iOS] Retrieve device locale in full (specific) format from ObjectiveC APIs by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111612](https://redirect.github.com/dotnet/runtime/pull/111612) - \[release/9.0-staging] Add workflow to prevent merging a PR when the `NO-MERGE` label is applied by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111961](https://redirect.github.com/dotnet/runtime/pull/111961) - \[release/9.0-staging] Use alternative format string specifier to ensure decimal point is present by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111444](https://redirect.github.com/dotnet/runtime/pull/111444) - \[release/9.0-staging] Fixed android build with NDK 23 by [@​jkurdek](https://redirect.github.com/jkurdek) in [https://github.com/dotnet/runtime/pull/111696](https://redirect.github.com/dotnet/runtime/pull/111696) - \[release/9.0-staging] Fix UNC paths by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111499](https://redirect.github.com/dotnet/runtime/pull/111499) - \[release/9.0-staging] \[mono] \[llvm-aot] Fixed storing Vector3 into memory by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/111069](https://redirect.github.com/dotnet/runtime/pull/111069) - \[release/9.0] Remove explicit \__compact_unwind entries from x64 assembler by [@​filipnavara](https://redirect.github.com/filipnavara) in [https://github.com/dotnet/runtime/pull/112204](https://redirect.github.com/dotnet/runtime/pull/112204) - Update branding to 9.0.3 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/runtime/pull/112144](https://redirect.github.com/dotnet/runtime/pull/112144) - \[release/9.0-staging] Update dependencies from dotnet/xharness by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111606](https://redirect.github.com/dotnet/runtime/pull/111606) - \[release/9.0] Update dependencies from dotnet/emsdk by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111891](https://redirect.github.com/dotnet/runtime/pull/111891) - \[release/9.0] Update dependencies from dotnet/emsdk by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/112189](https://redirect.github.com/dotnet/runtime/pull/112189) - \[release/9.0-staging] Update dependencies from dotnet/icu by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111519](https://redirect.github.com/dotnet/runtime/pull/111519) - \[release/9.0-staging] Update dependencies from dotnet/icu by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/112121](https://redirect.github.com/dotnet/runtime/pull/112121) - \[release/9.0-staging] Update dependencies from dotnet/runtime-assets by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111737](https://redirect.github.com/dotnet/runtime/pull/111737) - \[release/9.0-staging] Fix shimmed implementation of TryGetHashAndReset to handle HMAC. by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/112015](https://redirect.github.com/dotnet/runtime/pull/112015) - Remove Windows 8.1 from test queues by [@​agocke](https://redirect.github.com/agocke) in [https://github.com/dotnet/runtime/pull/112056](https://redirect.github.com/dotnet/runtime/pull/112056) - \[release/9.0-staging] Update dependencies from dotnet/source-build-reference-packages by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111603](https://redirect.github.com/dotnet/runtime/pull/111603) - \[browser] Remove experimental args from NodeJS WBT runner by [@​maraf](https://redirect.github.com/maraf) in [https://github.com/dotnet/runtime/pull/111655](https://redirect.github.com/dotnet/runtime/pull/111655) - \[release/9.0-staging] Update dependencies from dotnet/sdk by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111607](https://redirect.github.com/dotnet/runtime/pull/111607) - \[release/9.0-staging] Update dependencies from dotnet/roslyn-analyzers by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111826](https://redirect.github.com/dotnet/runtime/pull/111826) - \[release/9.0-staging] Update dependencies from dotnet/hotreload-utils by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111885](https://redirect.github.com/dotnet/runtime/pull/111885) - \[release/9.0-staging] Update dependencies from dotnet/cecil by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/112122](https://redirect.github.com/dotnet/runtime/pull/112122) - \[release/9.0-staging] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/112225](https://redirect.github.com/dotnet/runtime/pull/112225) - \[release/9.0-staging] Update dependencies from dotnet/icu by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/112261](https://redirect.github.com/dotnet/runtime/pull/112261) - \[automated] Merge branch 'release/9.0' => 'release/9.0-staging' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/runtime/pull/112219](https://redirect.github.com/dotnet/runtime/pull/112219) - \[release/9.0-staging] Update dependencies from dotnet/xharness by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/112340](https://redirect.github.com/dotnet/runtime/pull/112340) - \[release/9.0-staging] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/runtime/pull/111483](https://redirect.github.com/dotnet/runtime/pull/111483) - Backport pr 111723 to 9.0 staging by [@​StephenMolloy](https://redirect.github.com/StephenMolloy) in [https://github.com/dotnet/runtime/pull/112322](https://redirect.github.com/dotnet/runtime/pull/112322) - \[manual] Merge release/9.0-staging into release/9.0 by [@​carlossanlop](https://redirect.github.com/carlossanlop) in [https://github.com/dotnet/runtime/pull/112382](https://redirect.github.com/dotnet/runtime/pull/112382) - \[9.0] Backport labeling workflow changes by [@​carlossanlop](https://redirect.github.com/carlossanlop) in [https://github.com/dotnet/runtime/pull/112240](https://redirect.github.com/dotnet/runtime/pull/112240) - \[9.0] Move release/9.0 localization back to main too by [@​carlossanlop](https://redirect.github.com/carlossanlop) in [https://github.com/dotnet/runtime/pull/112443](https://redirect.github.com/dotnet/runtime/pull/112443) - Merging internal commits for release/9.0 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/runtime/pull/112453](https://redirect.github.com/dotnet/runtime/pull/112453) **Full Changelog**: https://github.com/dotnet/runtime/compare/v9.0.2...v9.0.3
dotnet/extensions (Microsoft.Extensions.Diagnostics.Testing) ### [`v9.3.0`](https://redirect.github.com/dotnet/extensions/releases/tag/v9.3.0) #### What's Changed - Move ResourceUtilizationInstruments to Shared project by [@​amadeuszl](https://redirect.github.com/amadeuszl) in [https://github.com/dotnet/extensions/pull/5844](https://redirect.github.com/dotnet/extensions/pull/5844) - Remove Tailwind and NPM dependency from chat template by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5846](https://redirect.github.com/dotnet/extensions/pull/5846) - Branding updates for 9.3 by [@​joperezr](https://redirect.github.com/joperezr) in [https://github.com/dotnet/extensions/pull/5847](https://redirect.github.com/dotnet/extensions/pull/5847) - Use an environment variable for configuring Ollama integration tests by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5851](https://redirect.github.com/dotnet/extensions/pull/5851) - Use unsafe relaxed escaping in `AIJsonUtilities.DefaultOptions`. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5850](https://redirect.github.com/dotnet/extensions/pull/5850) - Move AIFunction parameter schematization from parameter level to function level. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5826](https://redirect.github.com/dotnet/extensions/pull/5826) - Fix IChatClient integration tests after relaxed JSON update by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5859](https://redirect.github.com/dotnet/extensions/pull/5859) - Automate chat template JS dependency updates by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5853](https://redirect.github.com/dotnet/extensions/pull/5853) - Address M.E.AI API feedback by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5860](https://redirect.github.com/dotnet/extensions/pull/5860) - Add AsChatClient for OpenAI's AssistantClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5852](https://redirect.github.com/dotnet/extensions/pull/5852) - Some docs fixes by [@​gewarren](https://redirect.github.com/gewarren) in [https://github.com/dotnet/extensions/pull/5861](https://redirect.github.com/dotnet/extensions/pull/5861) - AI Templates - Fixes for Ollama and OpenAI scenarios by [@​jmatthiesen](https://redirect.github.com/jmatthiesen) in [https://github.com/dotnet/extensions/pull/5855](https://redirect.github.com/dotnet/extensions/pull/5855) - Update CODEOWNERS in preparation for upcoming MEAI.Evaluation libraries by [@​shyamnamboodiripad](https://redirect.github.com/shyamnamboodiripad) in [https://github.com/dotnet/extensions/pull/5872](https://redirect.github.com/dotnet/extensions/pull/5872) - Update to {Azure.AI.}OpenAI 2.2.0-beta.1 by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5869](https://redirect.github.com/dotnet/extensions/pull/5869) - Rename IChatClient members and corresponding types by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5870](https://redirect.github.com/dotnet/extensions/pull/5870) - Fix up a few more stale references to Complete{Streaming}Async (mainly tests) by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5876](https://redirect.github.com/dotnet/extensions/pull/5876) - Update test app for OpenAI NativeAOT support by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5877](https://redirect.github.com/dotnet/extensions/pull/5877) - Make IEmbeddingGenerator contravariant on TInput by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5879](https://redirect.github.com/dotnet/extensions/pull/5879) - Fix a few more straggler "Complete" references by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5881](https://redirect.github.com/dotnet/extensions/pull/5881) - Add .npmrc file by [@​wtgodbe](https://redirect.github.com/wtgodbe) in [https://github.com/dotnet/extensions/pull/5878](https://redirect.github.com/dotnet/extensions/pull/5878) - Merge internal changes by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5883](https://redirect.github.com/dotnet/extensions/pull/5883) - Enable API compat validation by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5871](https://redirect.github.com/dotnet/extensions/pull/5871) - Generalise how package version isn't stabilized for dev/preview stages by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5864](https://redirect.github.com/dotnet/extensions/pull/5864) - Use UseZeroToOneRangeForMetrics in Health Checks by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5849](https://redirect.github.com/dotnet/extensions/pull/5849) - Add service lifetime support to DI helpers. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5880](https://redirect.github.com/dotnet/extensions/pull/5880) - \[main] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/extensions/pull/5890](https://redirect.github.com/dotnet/extensions/pull/5890) - Add MEAI.Evaluation libraries by [@​shyamnamboodiripad](https://redirect.github.com/shyamnamboodiripad) in [https://github.com/dotnet/extensions/pull/5873](https://redirect.github.com/dotnet/extensions/pull/5873) - Remove the `ref int` parameter from `ChatConversationEvaluator.CanRenderAsync()` by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5896](https://redirect.github.com/dotnet/extensions/pull/5896) - Replace `AIFunctionParameterMetadata` with `MethodInfo` by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5886](https://redirect.github.com/dotnet/extensions/pull/5886) - Fix propagation of ChatThreadId in ToChatResponse{Async} by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5899](https://redirect.github.com/dotnet/extensions/pull/5899) - Couple of minor changes around doc comments by [@​shyamnamboodiripad](https://redirect.github.com/shyamnamboodiripad) in [https://github.com/dotnet/extensions/pull/5902](https://redirect.github.com/dotnet/extensions/pull/5902) - Build evaluation report in CI and when enabled by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5903](https://redirect.github.com/dotnet/extensions/pull/5903) - Rename AIFunctionFactoryOptions and make all properties nullable. by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5906](https://redirect.github.com/dotnet/extensions/pull/5906) - Update Azure.AI.Inference to 1.0.0-beta.3 by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5904](https://redirect.github.com/dotnet/extensions/pull/5904) - Add marker AITool for enabling code interpreters by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5898](https://redirect.github.com/dotnet/extensions/pull/5898) - Fix analyzer reference in AuditReports package by [@​dariusclay](https://redirect.github.com/dariusclay) in [https://github.com/dotnet/extensions/pull/5894](https://redirect.github.com/dotnet/extensions/pull/5894) - Pre-Build TypeScript packages before CI builds by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5907](https://redirect.github.com/dotnet/extensions/pull/5907) - Reset code coverage baselines by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5912](https://redirect.github.com/dotnet/extensions/pull/5912) - Update CHANGELOGs for next release by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5911](https://redirect.github.com/dotnet/extensions/pull/5911) - Use Environment.WorkingSet in WindowsSnapshotProvider by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5874](https://redirect.github.com/dotnet/extensions/pull/5874) - Tweak a comment in FunctionInvokingChatClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5910](https://redirect.github.com/dotnet/extensions/pull/5910) - Remove unnecessary compilations causing duplicate metric generators by [@​dariusclay](https://redirect.github.com/dariusclay) in [https://github.com/dotnet/extensions/pull/5916](https://redirect.github.com/dotnet/extensions/pull/5916) - Update baseline by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5918](https://redirect.github.com/dotnet/extensions/pull/5918) - Make MetadataExtractor & MetricsReports in Microsoft.Extensions.AuditReports create directories \[Same as ComplianceReport] by [@​IbrahimMNada](https://redirect.github.com/IbrahimMNada) in [https://github.com/dotnet/extensions/pull/5919](https://redirect.github.com/dotnet/extensions/pull/5919) - Update template to newest M.E.AI version by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5923](https://redirect.github.com/dotnet/extensions/pull/5923) - Introduce type converter for DataClassification by [@​dariusclay](https://redirect.github.com/dariusclay) in [https://github.com/dotnet/extensions/pull/5887](https://redirect.github.com/dotnet/extensions/pull/5887) - Reinstate caching in schema generation by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5908](https://redirect.github.com/dotnet/extensions/pull/5908) - Add GetRequiredService extension for IChatClient/EmbeddingGenerator by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5930](https://redirect.github.com/dotnet/extensions/pull/5930) - Update M.E.AI READMEs by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5925](https://redirect.github.com/dotnet/extensions/pull/5925) - Use structured output for RTC evaluator by [@​shyamnamboodiripad](https://redirect.github.com/shyamnamboodiripad) in [https://github.com/dotnet/extensions/pull/5945](https://redirect.github.com/dotnet/extensions/pull/5945) - Upgrade `slngen` to avoid crash with newer versions of VS by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5932](https://redirect.github.com/dotnet/extensions/pull/5932) - Update coverage baseline by [@​shyamnamboodiripad](https://redirect.github.com/shyamnamboodiripad) in [https://github.com/dotnet/extensions/pull/5948](https://redirect.github.com/dotnet/extensions/pull/5948) - Make sure npm build tasks happen reliably and only once. by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5926](https://redirect.github.com/dotnet/extensions/pull/5926) - Address API feedback on M.E.AI by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5954](https://redirect.github.com/dotnet/extensions/pull/5954) - Template updates - Added an Overview/README, support of managed identity by [@​jmatthiesen](https://redirect.github.com/jmatthiesen) in [https://github.com/dotnet/extensions/pull/5897](https://redirect.github.com/dotnet/extensions/pull/5897) - Chat template updates following threat model review by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5961](https://redirect.github.com/dotnet/extensions/pull/5961) - Finalizing template name and sample data by [@​jmatthiesen](https://redirect.github.com/jmatthiesen) in [https://github.com/dotnet/extensions/pull/5966](https://redirect.github.com/dotnet/extensions/pull/5966) - Chat template static files layout update by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/5968](https://redirect.github.com/dotnet/extensions/pull/5968) - Enable building and signing VSIX for extension by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5969](https://redirect.github.com/dotnet/extensions/pull/5969) - Fix RTC evaluator by [@​shyamnamboodiripad](https://redirect.github.com/shyamnamboodiripad) in [https://github.com/dotnet/extensions/pull/5971](https://redirect.github.com/dotnet/extensions/pull/5971) - Move Azure DevOps extension build until after the transport package build by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5973](https://redirect.github.com/dotnet/extensions/pull/5973) - HybridCache: release to GA for 9.3 by [@​mgravell](https://redirect.github.com/mgravell) in [https://github.com/dotnet/extensions/pull/5931](https://redirect.github.com/dotnet/extensions/pull/5931) - Automate chat template .NET dependency updates by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5946](https://redirect.github.com/dotnet/extensions/pull/5946) - Explicitly specify VSIX file to sign to fix internal official build. by [@​peterwald](https://redirect.github.com/peterwald) in [https://github.com/dotnet/extensions/pull/5978](https://redirect.github.com/dotnet/extensions/pull/5978) - Remove gitignore from AI chat template by [@​halter73](https://redirect.github.com/halter73) in [https://github.com/dotnet/extensions/pull/5972](https://redirect.github.com/dotnet/extensions/pull/5972) - Addressing dogfooding feedback, adding GitHub Models support by [@​jmatthiesen](https://redirect.github.com/jmatthiesen) in [https://github.com/dotnet/extensions/pull/5974](https://redirect.github.com/dotnet/extensions/pull/5974) - Add `Microsoft.Extensions.AI.Templates` third party notices by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5949](https://redirect.github.com/dotnet/extensions/pull/5949) - Move example PDFs into wwwroot/Data by [@​jeffhandley](https://redirect.github.com/jeffhandley) in [https://github.com/dotnet/extensions/pull/5982](https://redirect.github.com/dotnet/extensions/pull/5982) - Remove unnecessary type from the source generator by [@​eiriktsarpalis](https://redirect.github.com/eiriktsarpalis) in [https://github.com/dotnet/extensions/pull/5985](https://redirect.github.com/dotnet/extensions/pull/5985) - HybridCache JSON serialization improvements by [@​mgravell](https://redirect.github.com/mgravell) in [https://github.com/dotnet/extensions/pull/5979](https://redirect.github.com/dotnet/extensions/pull/5979) - Improve debugger display for ChatMessage by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/5988](https://redirect.github.com/dotnet/extensions/pull/5988) - Address missing reference to Microsoft.Extensions.AI.OpenAI by [@​artl93](https://redirect.github.com/artl93) in [https://github.com/dotnet/extensions/pull/5984](https://redirect.github.com/dotnet/extensions/pull/5984) - Add logging sampling by [@​evgenyfedorov2](https://redirect.github.com/evgenyfedorov2) in [https://github.com/dotnet/extensions/pull/5574](https://redirect.github.com/dotnet/extensions/pull/5574) - Fix chat template content generation build flakiness by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/5990](https://redirect.github.com/dotnet/extensions/pull/5990) - Re-enabled forward main->dev merges by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5991](https://redirect.github.com/dotnet/extensions/pull/5991) - Add context to getting started with using Azure AI Search by [@​artl93](https://redirect.github.com/artl93) in [https://github.com/dotnet/extensions/pull/5993](https://redirect.github.com/dotnet/extensions/pull/5993) - Remove API key comment when using managed identity by [@​artl93](https://redirect.github.com/artl93) in [https://github.com/dotnet/extensions/pull/5996](https://redirect.github.com/dotnet/extensions/pull/5996) - Touch-up to template readme.md by [@​artl93](https://redirect.github.com/artl93) in [https://github.com/dotnet/extensions/pull/6006](https://redirect.github.com/dotnet/extensions/pull/6006) - Address vector dimension mismatch when using ollama with Azure AI Search by [@​artl93](https://redirect.github.com/artl93) in [https://github.com/dotnet/extensions/pull/6001](https://redirect.github.com/dotnet/extensions/pull/6001) - Make AnonymousDelegatingEmbeddingGenerator internal by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/6000](https://redirect.github.com/dotnet/extensions/pull/6000) - AI Template Preview 1 - ID cleanup & link to survey by [@​jmatthiesen](https://redirect.github.com/jmatthiesen) in [https://github.com/dotnet/extensions/pull/5994](https://redirect.github.com/dotnet/extensions/pull/5994) - Expose default cache key use by DistributedCachingCacheClient by [@​stephentoub](https://redirect.github.com/stephentoub) in [https://github.com/dotnet/extensions/pull/6002](https://redirect.github.com/dotnet/extensions/pull/6002) - Only show real citations by [@​SteveSandersonMS](https://redirect.github.com/SteveSandersonMS) in [https://github.com/dotnet/extensions/pull/6012](https://redirect.github.com/dotnet/extensions/pull/6012) - Add missing Maestro registrations by [@​RussKie](https://redirect.github.com/RussKie) in [https://github.com/dotnet/extensions/pull/5995](https://redirect.github.com/dotnet/extensions/pull/5995) - Fix flow for new dependencies by [@​MackinnonBuck](https://redirect.github.com/MackinnonBuck) in [https://github.com/dotnet/extensions/pull/6017](https://redirect.github.com/dotnet/extensions/pull/6017) - Add a link to the Brief Survey from the template's README by [@​jeffhandley](https://redirect.github.com/jeffhandley) in [https://github.com/dotnet/extensions/pull/6015](https://redirect.github.com/dotnet/extensions/pull/6015) - HybridCache: enforce L2 expiration by [@​mgravell](https://redirect.github.com/mgravell) in [https://github.com/dotnet/extensions/pull/5987](https://redirect.github.com/dotnet/extensions/pull/5987) - Update AI Chat Web readme by [@​jongalloway](https://redirect.github.com/jongalloway) in [https://github.com/dotnet/extensions/pull/6014](https://redirect.github.com/dotnet/extensions/pull/6014) - Generate the vector store index name using project name. Fix symbol handling for CSS and dimensions. by [@​jeffhandley](https://redirect.github.com/jeffhandley) in [https://github.com/dotnet/extensions/pull/6025](https://redirect.github.com/dotnet/extensions/pull/6025) #### New Contributors - [@​jmatthiesen](https://redirect.github.com/jmatthiesen) made their first contribution in [https://github.com/dotnet/extensions/pull/5855](https://redirect.github.com/dotnet/extensions/pull/5855) - [@​peterwald](https://redirect.github.com/peterwald) made their first contribution in [https://github.com/dotnet/extensions/pull/5896](https://redirect.github.com/dotnet/extensions/pull/5896) - [@​artl93](https://redirect.github.com/artl93) made their first contribution in [https://github.com/dotnet/extensions/pull/5984](https://redirect.github.com/dotnet/extensions/pull/5984) - [@​jongalloway](https://redirect.github.com/jongalloway) made their first contribution in [https://github.com/dotnet/extensions/pull/6014](https://redirect.github.com/dotnet/extensions/pull/6014) **Full Changelog**: https://github.com/dotnet/extensions/compare/v9.2.0...v9.3.0
dotnet/sdk (dotnet-sdk) ### [`v9.0.201`](https://redirect.github.com/dotnet/sdk/releases/tag/v9.0.201): .NET 9.0.3 [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v9.0.200...v9.0.201) [Release](https://redirect.github.com/dotnet/core/releases/tag/v9.0.3) #### What's Changed - \[release/8.0.1xx] Update dependencies from dotnet/source-build-externals by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/45866](https://redirect.github.com/dotnet/sdk/pull/45866) - \[release/8.0.1xx] Update dependencies from dotnet/source-build-reference-packages by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/45889](https://redirect.github.com/dotnet/sdk/pull/45889) - \[release/8.0.1xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/45884](https://redirect.github.com/dotnet/sdk/pull/45884) - \[release/8.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/45890](https://redirect.github.com/dotnet/sdk/pull/45890) - \[automated] Merge branch 'release/8.0.1xx' => 'release/8.0.3xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/45912](https://redirect.github.com/dotnet/sdk/pull/45912) - \[release/8.0.1xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/45915](https://redirect.github.com/dotnet/sdk/pull/45915) - \[automated] Merge branch 'release/8.0.1xx' => 'release/8.0.3xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/45922](https://redirect.github.com/dotnet/sdk/pull/45922) - \[release/8.0.3xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/45919](https://redirect.github.com/dotnet/sdk/pull/45919) - Update dependencies from dotnet/deployment-tools by [@​joeloff](https://redirect.github.com/joeloff) in [https://github.com/dotnet/sdk/pull/45970](https://redirect.github.com/dotnet/sdk/pull/45970) - Update dependencies from dotnet/deployment-tools by [@​joeloff](https://redirect.github.com/joeloff) in [https://github.com/dotnet/sdk/pull/45971](https://redirect.github.com/dotnet/sdk/pull/45971) - \[release/8.0.3xx] Update dependencies from dotnet/roslyn by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/45989](https://redirect.github.com/dotnet/sdk/pull/45989) - \[automated] Merge branch 'release/8.0.1xx' => 'release/8.0.3xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/45978](https://redirect.github.com/dotnet/sdk/pull/45978) - \[automated] Merge branch 'release/8.0.3xx' => 'release/8.0.4xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/45976](https://redirect.github.com/dotnet/sdk/pull/45976) - Merging internal commits for release/8.0.1xx by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/45963](https://redirect.github.com/dotnet/sdk/pull/45963) - \[release/8.0.4xx] Update dependencies from dotnet/msbuild by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46028](https://redirect.github.com/dotnet/sdk/pull/46028) - \[release/9.0.1xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46042](https://redirect.github.com/dotnet/sdk/pull/46042) - \[release/9.0.1xx] Update dependencies from dotnet/msbuild by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46030](https://redirect.github.com/dotnet/sdk/pull/46030) - \[release/9.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46032](https://redirect.github.com/dotnet/sdk/pull/46032) - Merging internal commits for release/8.0.3xx by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/46017](https://redirect.github.com/dotnet/sdk/pull/46017) - Merging internal commits for release/8.0.4xx by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/46018](https://redirect.github.com/dotnet/sdk/pull/46018) - \[release/8.0.1xx] Update dependencies from dotnet/msbuild by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46020](https://redirect.github.com/dotnet/sdk/pull/46020) - \[release/8.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46025](https://redirect.github.com/dotnet/sdk/pull/46025) - \[release/8.0.4xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46029](https://redirect.github.com/dotnet/sdk/pull/46029) - \[release/8.0.3xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46027](https://redirect.github.com/dotnet/sdk/pull/46027) - \[release/8.0.1xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46038](https://redirect.github.com/dotnet/sdk/pull/46038) - \[release/8.0.3xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46078](https://redirect.github.com/dotnet/sdk/pull/46078) - \[release/8.0.3xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46079](https://redirect.github.com/dotnet/sdk/pull/46079) - \[release/8.0.4xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46080](https://redirect.github.com/dotnet/sdk/pull/46080) - \[release/8.0.4xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46081](https://redirect.github.com/dotnet/sdk/pull/46081) - \[release/8.0.1xx] Update dependencies from dotnet/arcade by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46077](https://redirect.github.com/dotnet/sdk/pull/46077) - \[release/9.0] Remove alpine n-1 leg by [@​ellahathaway](https://redirect.github.com/ellahathaway) in [https://github.com/dotnet/sdk/pull/46068](https://redirect.github.com/dotnet/sdk/pull/46068) - \[release/8.0.4xx] Fix defaulting of which Runtime(s) to containerize that led to always publishing multiple containers by [@​baronfel](https://redirect.github.com/baronfel) in [https://github.com/dotnet/sdk/pull/46067](https://redirect.github.com/dotnet/sdk/pull/46067) - Backport 46067 to 9.0.1xx by [@​baronfel](https://redirect.github.com/baronfel) in [https://github.com/dotnet/sdk/pull/46169](https://redirect.github.com/dotnet/sdk/pull/46169) - \[release/9.0.1xx] Update scancode to latest by [@​ellahathaway](https://redirect.github.com/ellahathaway) in [https://github.com/dotnet/sdk/pull/46004](https://redirect.github.com/dotnet/sdk/pull/46004) - Update loc to target 9.0.3xx by [@​Forgind](https://redirect.github.com/Forgind) in [https://github.com/dotnet/sdk/pull/46010](https://redirect.github.com/dotnet/sdk/pull/46010) - Fix bug when adding solution folder containing `..` by [@​edvilme](https://redirect.github.com/edvilme) in [https://github.com/dotnet/sdk/pull/46456](https://redirect.github.com/dotnet/sdk/pull/46456) - Update branding to 9.0.201 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/46523](https://redirect.github.com/dotnet/sdk/pull/46523) - Update branding to 9.0.104 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/46522](https://redirect.github.com/dotnet/sdk/pull/46522) - Update branding to 8.0.407 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/46521](https://redirect.github.com/dotnet/sdk/pull/46521) - Update branding to 8.0.114 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/46519](https://redirect.github.com/dotnet/sdk/pull/46519) - Update branding to 8.0.310 by [@​vseanreesermsft](https://redirect.github.com/vseanreesermsft) in [https://github.com/dotnet/sdk/pull/46520](https://redirect.github.com/dotnet/sdk/pull/46520) - Port a set of test fixes for the templating tests and some feed tests to 9.0.1xx ([#​45549](https://redirect.github.com/dotnet/sdk/issues/45549)) by [@​marcpopMSFT](https://redirect.github.com/marcpopMSFT) in [https://github.com/dotnet/sdk/pull/46431](https://redirect.github.com/dotnet/sdk/pull/46431) - Use official templates even for PRs when run internal by [@​marcpopMSFT](https://redirect.github.com/marcpopMSFT) in [https://github.com/dotnet/sdk/pull/46405](https://redirect.github.com/dotnet/sdk/pull/46405) - \[release/9.0.2xx] Update dependencies from dotnet/scenario-tests by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46396](https://redirect.github.com/dotnet/sdk/pull/46396) - \[automated] Merge branch 'release/9.0.1xx' => 'release/9.0.2xx' by [@​github-actions](https://redirect.github.com/github-actions) in [https://github.com/dotnet/sdk/pull/46166](https://redirect.github.com/dotnet/sdk/pull/46166) - \[release/8.0.3xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46116](https://redirect.github.com/dotnet/sdk/pull/46116) - \[release/9.0.1xx] Update dependencies from dotnet/scenario-tests by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46135](https://redirect.github.com/dotnet/sdk/pull/46135) - \[release/8.0.1xx] Update dependencies from dotnet/source-build-reference-packages by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46114](https://redirect.github.com/dotnet/sdk/pull/46114) - \[release/8.0.4xx] Update dependencies from dotnet/templating by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46118](https://redirect.github.com/dotnet/sdk/pull/46118) - \[release/8.0.3xx] Update dependencies from dotnet/msbuild by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46149](https://redirect.github.com/dotnet/sdk/pull/46149) - \[release/9.0.1xx] Update dependencies from dotnet/msbuild by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46153](https://redirect.github.com/dotnet/sdk/pull/46153) - \[release/8.0.1xx] Update dependencies from dotnet/msbuild by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46159](https://redirect.github.com/dotnet/sdk/pull/46159) - \[release/8.0.4xx] Update dependencies from dotnet/msbuild by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46196](https://redirect.github.com/dotnet/sdk/pull/46196) - \[release/9.0.2xx] Update dependencies from microsoft/vstest by [@​dotnet-maestro](https://redirect.github.com/dotnet-maestro) in [https://github.com/dotnet/sdk/pull/46265](https://redirect.github.com/dotnet/sdk/pull/46265) - \[release/9.0.2xx] Update dependencies from dotnet/roslyn-analyzers by [@​dotnet-maestro](https://redirect.
--- ### 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. πŸ‘» **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] 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> --- Directory.Packages.props | 20 ++++++++++---------- global.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5f5b23d0..da04b8fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,14 +5,14 @@ - - - - - - - - + + + + + + + + @@ -22,13 +22,13 @@ - + - + diff --git a/global.json b/global.json index 12741f85..36fe7d0f 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMajor", - "version": "9.0.200", + "version": "9.0.201", "allowPrerelease": false } } From 2e2c4898479b3544d663c08ddd2dc011ca482b43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:58:03 +0000 Subject: [PATCH 275/316] chore(deps): update actions/setup-dotnet digest to 67a3573 (#402) 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 | |---|---|---|---| | [actions/setup-dotnet](https://redirect.github.com/actions/setup-dotnet) | action | digest | `3951f0d` -> `67a3573` | --- ### 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/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 378f22d5..61a5e28e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -61,7 +61,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 03f61e8f..603349e8 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index abf45a70..e96d5ec0 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup .NET SDK - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: 9.0.x diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7425a82c..f4a3d93c 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 221044ff..b5b17a1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -70,7 +70,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 75468d28ba4d8200c7199fe89d6d1a63f3bdd674 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:21:34 +0000 Subject: [PATCH 276/316] chore(deps): update dependency system.valuetuple to 4.6.0 (#403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [System.ValueTuple](https://redirect.github.com/dotnet/maintenance-packages) | `4.5.0` -> `4.6.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/System.ValueTuple/4.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/System.ValueTuple/4.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/System.ValueTuple/4.5.0/4.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/System.ValueTuple/4.5.0/4.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### 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> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index da04b8fb..c30681f6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ - + From 73a504022d8ba4cbe508a4f0b76f9b73f58c17a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:58:32 +0000 Subject: [PATCH 277/316] chore(deps): update github/codeql-action digest to 5f8171a (#404) 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://redirect.github.com/github/codeql-action) | action | digest | `6bb031a` -> `5f8171a` | --- ### 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 94a8f0a2..834f3f8b 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@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 + uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # 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@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 + uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # 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@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 + uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3 From a4beaaea375b3184578d259cd5ca481d23055a54 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:58:54 +0000 Subject: [PATCH 278/316] chore(deps): update dependency dotnet-sdk to v9.0.202 (#405) 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 | |---|---|---|---| | [dotnet-sdk](https://redirect.github.com/dotnet/sdk) | dotnet-sdk | patch | `9.0.201` -> `9.0.202` | --- ### Release Notes
dotnet/sdk (dotnet-sdk) ### [`v9.0.202`](https://redirect.github.com/dotnet/sdk/compare/v9.0.201...v9.0.202) [Compare Source](https://redirect.github.com/dotnet/sdk/compare/v9.0.201...v9.0.202)
--- ### 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> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 36fe7d0f..46506cda 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMajor", - "version": "9.0.201", + "version": "9.0.202", "allowPrerelease": false } } From 16c92b7814f49aceab6e6d46a8835c2bdc0f3363 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:18:28 +0000 Subject: [PATCH 279/316] chore(deps): update actions/upload-artifact action to v4.6.2 (#406) 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 | |---|---|---|---| | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | patch | `v4.6.1` -> `v4.6.2` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.6.2`](https://redirect.github.com/actions/upload-artifact/releases/tag/v4.6.2) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v4.6.1...v4.6.2) #### What's Changed - Update to use artifact 2.3.2 package & prepare for new upload-artifact release by [@​salmanmkc](https://redirect.github.com/salmanmkc) in [https://github.com/actions/upload-artifact/pull/685](https://redirect.github.com/actions/upload-artifact/pull/685) #### New Contributors - [@​salmanmkc](https://redirect.github.com/salmanmkc) made their first contribution in [https://github.com/actions/upload-artifact/pull/685](https://redirect.github.com/actions/upload-artifact/pull/685) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4...v4.6.2
--- ### 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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61a5e28e..06746aff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: nupkgs path: src/**/*.nupkg From ae9fc79bcb9847efcb62673f5aa59df403cece78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:38:29 +0000 Subject: [PATCH 280/316] chore(deps): update github/codeql-action digest to 1b549b9 (#407) 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://redirect.github.com/github/codeql-action) | action | digest | `5f8171a` -> `1b549b9` | --- ### 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 834f3f8b..5fc869ca 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@5f8171a638ada777af81d42b55959a643bb29017 # v3 + uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # 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@5f8171a638ada777af81d42b55959a643bb29017 # v3 + uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # 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@5f8171a638ada777af81d42b55959a643bb29017 # v3 + uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 From f1bf7a6b02e7d1d846bf740c9942dcd3f07a83d8 Mon Sep 17 00:00:00 2001 From: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:03:05 -0400 Subject: [PATCH 281/316] chore(main): release 2.3.2 (#372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.3.2](https://github.com/open-feature/dotnet-sdk/compare/v2.3.1...v2.3.2) (2025-03-24) ### πŸ› Bug Fixes * Address issue with newline characters when running Logging Hook Unit Tests on linux ([#374](https://github.com/open-feature/dotnet-sdk/issues/374)) ([a98334e](https://github.com/open-feature/dotnet-sdk/commit/a98334edfc0a6a14ff60e362bd7aa198b70ff255)) * Remove virtual GetEventChannel from FeatureProvider ([#401](https://github.com/open-feature/dotnet-sdk/issues/401)) ([00a4e4a](https://github.com/open-feature/dotnet-sdk/commit/00a4e4ab2ccb8984cd3ca57bad6d25e688b1cf8c)) * Update project name in solution file ([#380](https://github.com/open-feature/dotnet-sdk/issues/380)) ([1f13258](https://github.com/open-feature/dotnet-sdk/commit/1f13258737fa051289d51cf5a064e03b0dc936c8)) ### 🧹 Chore * Correct LoggingHookTest timestamp handling. ([#386](https://github.com/open-feature/dotnet-sdk/issues/386)) ([c69a6e5](https://github.com/open-feature/dotnet-sdk/commit/c69a6e5d71a6d652017a0d46c8390554a1dec59e)) * **deps:** update actions/setup-dotnet digest to 67a3573 ([#402](https://github.com/open-feature/dotnet-sdk/issues/402)) ([2e2c489](https://github.com/open-feature/dotnet-sdk/commit/2e2c4898479b3544d663c08ddd2dc011ca482b43)) * **deps:** update actions/upload-artifact action to v4.6.1 ([#385](https://github.com/open-feature/dotnet-sdk/issues/385)) ([accf571](https://github.com/open-feature/dotnet-sdk/commit/accf57181b34c600cb775a93b173f644d8c445d1)) * **deps:** update actions/upload-artifact action to v4.6.2 ([#406](https://github.com/open-feature/dotnet-sdk/issues/406)) ([16c92b7](https://github.com/open-feature/dotnet-sdk/commit/16c92b7814f49aceab6e6d46a8835c2bdc0f3363)) * **deps:** update codecov/codecov-action action to v5.4.0 ([#392](https://github.com/open-feature/dotnet-sdk/issues/392)) ([06e4e3a](https://github.com/open-feature/dotnet-sdk/commit/06e4e3a7ee11aff5c53eeba2259a840956bc4d5d)) * **deps:** update dependency dotnet-sdk to v9.0.202 ([#405](https://github.com/open-feature/dotnet-sdk/issues/405)) ([a4beaae](https://github.com/open-feature/dotnet-sdk/commit/a4beaaea375b3184578d259cd5ca481d23055a54)) * **deps:** update dependency microsoft.net.test.sdk to 17.13.0 ([#375](https://github.com/open-feature/dotnet-sdk/issues/375)) ([7a735f8](https://github.com/open-feature/dotnet-sdk/commit/7a735f8d8b82b79b205f71716e5cf300a7fff276)) * **deps:** update dependency reqnroll.xunit to 2.3.0 ([#378](https://github.com/open-feature/dotnet-sdk/issues/378)) ([96ba568](https://github.com/open-feature/dotnet-sdk/commit/96ba5686c2ba31996603f464fe7e5df9efa01a92)) * **deps:** update dependency reqnroll.xunit to 2.4.0 ([#396](https://github.com/open-feature/dotnet-sdk/issues/396)) ([b30350b](https://github.com/open-feature/dotnet-sdk/commit/b30350bd49f4a8709b69a3eb2db1152d5a4b7f6c)) * **deps:** update dependency system.valuetuple to 4.6.0 ([#403](https://github.com/open-feature/dotnet-sdk/issues/403)) ([75468d2](https://github.com/open-feature/dotnet-sdk/commit/75468d28ba4d8200c7199fe89d6d1a63f3bdd674)) * **deps:** update dotnet monorepo ([#379](https://github.com/open-feature/dotnet-sdk/issues/379)) ([53ced91](https://github.com/open-feature/dotnet-sdk/commit/53ced9118ffcb8cda5142dc2f80465416922030b)) * **deps:** update dotnet monorepo to 9.0.2 ([#377](https://github.com/open-feature/dotnet-sdk/issues/377)) ([3bdc79b](https://github.com/open-feature/dotnet-sdk/commit/3bdc79bbaa8d73c4747916d307c431990397cdde)) * **deps:** update github/codeql-action digest to 1b549b9 ([#407](https://github.com/open-feature/dotnet-sdk/issues/407)) ([ae9fc79](https://github.com/open-feature/dotnet-sdk/commit/ae9fc79bcb9847efcb62673f5aa59df403cece78)) * **deps:** update github/codeql-action digest to 5f8171a ([#404](https://github.com/open-feature/dotnet-sdk/issues/404)) ([73a5040](https://github.com/open-feature/dotnet-sdk/commit/73a504022d8ba4cbe508a4f0b76f9b73f58c17a6)) * **deps:** update github/codeql-action digest to 6bb031a ([#398](https://github.com/open-feature/dotnet-sdk/issues/398)) ([9b6feab](https://github.com/open-feature/dotnet-sdk/commit/9b6feab50085ee7dfcca190fe42f583c072ae50d)) * **deps:** update github/codeql-action digest to 9e8d078 ([#371](https://github.com/open-feature/dotnet-sdk/issues/371)) ([e74e8e7](https://github.com/open-feature/dotnet-sdk/commit/e74e8e7a58d90e46bbcd5d7e9433545412e07bbd)) * **deps:** update github/codeql-action digest to b56ba49 ([#384](https://github.com/open-feature/dotnet-sdk/issues/384)) ([cc2990f](https://github.com/open-feature/dotnet-sdk/commit/cc2990ff8e7bf5148ab1cd867d9bfabfc0b7af8a)) * **deps:** update spec digest to 0cd553d ([#389](https://github.com/open-feature/dotnet-sdk/issues/389)) ([85075ac](https://github.com/open-feature/dotnet-sdk/commit/85075ac7f46783dd1bcfdbbe6bd10d81eb9adb8a)) * **deps:** update spec digest to 54952f3 ([#373](https://github.com/open-feature/dotnet-sdk/issues/373)) ([1e8b230](https://github.com/open-feature/dotnet-sdk/commit/1e8b2307369710ea0b5ae0e8a8f1f1293ea066dc)) * **deps:** update spec digest to a69f748 ([#382](https://github.com/open-feature/dotnet-sdk/issues/382)) ([4977542](https://github.com/open-feature/dotnet-sdk/commit/4977542515bff302c7a88f3fa301bb129d7ea8cf)) * remove FluentAssertions ([#361](https://github.com/open-feature/dotnet-sdk/issues/361)) ([4ecfd24](https://github.com/open-feature/dotnet-sdk/commit/4ecfd249181cf8fe372810a1fc3369347c6302fc)) * Replace SpecFlow with Reqnroll for testing framework ([#368](https://github.com/open-feature/dotnet-sdk/issues/368)) ([ed6ee2c](https://github.com/open-feature/dotnet-sdk/commit/ed6ee2c502b16e49c91c6363ae6b3f54401a85cb)), closes [#354](https://github.com/open-feature/dotnet-sdk/issues/354) * update release please repo, specify action permissions ([#369](https://github.com/open-feature/dotnet-sdk/issues/369)) ([63846ad](https://github.com/open-feature/dotnet-sdk/commit/63846ad1033399e9c84ad5946367c5eef2663b5b)) ### πŸ”„ Refactoring * Improve EventExecutor ([#393](https://github.com/open-feature/dotnet-sdk/issues/393)) ([46274a2](https://github.com/open-feature/dotnet-sdk/commit/46274a21d74b5cfffd4cfbc30e5e49e2dc1f256c)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index aca3a494..8d17a8e5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.1" + ".": "2.3.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9513b8..2d9109cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## [2.3.2](https://github.com/open-feature/dotnet-sdk/compare/v2.3.1...v2.3.2) (2025-03-24) + + +### πŸ› Bug Fixes + +* Address issue with newline characters when running Logging Hook Unit Tests on linux ([#374](https://github.com/open-feature/dotnet-sdk/issues/374)) ([a98334e](https://github.com/open-feature/dotnet-sdk/commit/a98334edfc0a6a14ff60e362bd7aa198b70ff255)) +* Remove virtual GetEventChannel from FeatureProvider ([#401](https://github.com/open-feature/dotnet-sdk/issues/401)) ([00a4e4a](https://github.com/open-feature/dotnet-sdk/commit/00a4e4ab2ccb8984cd3ca57bad6d25e688b1cf8c)) +* Update project name in solution file ([#380](https://github.com/open-feature/dotnet-sdk/issues/380)) ([1f13258](https://github.com/open-feature/dotnet-sdk/commit/1f13258737fa051289d51cf5a064e03b0dc936c8)) + + +### 🧹 Chore + +* Correct LoggingHookTest timestamp handling. ([#386](https://github.com/open-feature/dotnet-sdk/issues/386)) ([c69a6e5](https://github.com/open-feature/dotnet-sdk/commit/c69a6e5d71a6d652017a0d46c8390554a1dec59e)) +* **deps:** update actions/setup-dotnet digest to 67a3573 ([#402](https://github.com/open-feature/dotnet-sdk/issues/402)) ([2e2c489](https://github.com/open-feature/dotnet-sdk/commit/2e2c4898479b3544d663c08ddd2dc011ca482b43)) +* **deps:** update actions/upload-artifact action to v4.6.1 ([#385](https://github.com/open-feature/dotnet-sdk/issues/385)) ([accf571](https://github.com/open-feature/dotnet-sdk/commit/accf57181b34c600cb775a93b173f644d8c445d1)) +* **deps:** update actions/upload-artifact action to v4.6.2 ([#406](https://github.com/open-feature/dotnet-sdk/issues/406)) ([16c92b7](https://github.com/open-feature/dotnet-sdk/commit/16c92b7814f49aceab6e6d46a8835c2bdc0f3363)) +* **deps:** update codecov/codecov-action action to v5.4.0 ([#392](https://github.com/open-feature/dotnet-sdk/issues/392)) ([06e4e3a](https://github.com/open-feature/dotnet-sdk/commit/06e4e3a7ee11aff5c53eeba2259a840956bc4d5d)) +* **deps:** update dependency dotnet-sdk to v9.0.202 ([#405](https://github.com/open-feature/dotnet-sdk/issues/405)) ([a4beaae](https://github.com/open-feature/dotnet-sdk/commit/a4beaaea375b3184578d259cd5ca481d23055a54)) +* **deps:** update dependency microsoft.net.test.sdk to 17.13.0 ([#375](https://github.com/open-feature/dotnet-sdk/issues/375)) ([7a735f8](https://github.com/open-feature/dotnet-sdk/commit/7a735f8d8b82b79b205f71716e5cf300a7fff276)) +* **deps:** update dependency reqnroll.xunit to 2.3.0 ([#378](https://github.com/open-feature/dotnet-sdk/issues/378)) ([96ba568](https://github.com/open-feature/dotnet-sdk/commit/96ba5686c2ba31996603f464fe7e5df9efa01a92)) +* **deps:** update dependency reqnroll.xunit to 2.4.0 ([#396](https://github.com/open-feature/dotnet-sdk/issues/396)) ([b30350b](https://github.com/open-feature/dotnet-sdk/commit/b30350bd49f4a8709b69a3eb2db1152d5a4b7f6c)) +* **deps:** update dependency system.valuetuple to 4.6.0 ([#403](https://github.com/open-feature/dotnet-sdk/issues/403)) ([75468d2](https://github.com/open-feature/dotnet-sdk/commit/75468d28ba4d8200c7199fe89d6d1a63f3bdd674)) +* **deps:** update dotnet monorepo ([#379](https://github.com/open-feature/dotnet-sdk/issues/379)) ([53ced91](https://github.com/open-feature/dotnet-sdk/commit/53ced9118ffcb8cda5142dc2f80465416922030b)) +* **deps:** update dotnet monorepo to 9.0.2 ([#377](https://github.com/open-feature/dotnet-sdk/issues/377)) ([3bdc79b](https://github.com/open-feature/dotnet-sdk/commit/3bdc79bbaa8d73c4747916d307c431990397cdde)) +* **deps:** update github/codeql-action digest to 1b549b9 ([#407](https://github.com/open-feature/dotnet-sdk/issues/407)) ([ae9fc79](https://github.com/open-feature/dotnet-sdk/commit/ae9fc79bcb9847efcb62673f5aa59df403cece78)) +* **deps:** update github/codeql-action digest to 5f8171a ([#404](https://github.com/open-feature/dotnet-sdk/issues/404)) ([73a5040](https://github.com/open-feature/dotnet-sdk/commit/73a504022d8ba4cbe508a4f0b76f9b73f58c17a6)) +* **deps:** update github/codeql-action digest to 6bb031a ([#398](https://github.com/open-feature/dotnet-sdk/issues/398)) ([9b6feab](https://github.com/open-feature/dotnet-sdk/commit/9b6feab50085ee7dfcca190fe42f583c072ae50d)) +* **deps:** update github/codeql-action digest to 9e8d078 ([#371](https://github.com/open-feature/dotnet-sdk/issues/371)) ([e74e8e7](https://github.com/open-feature/dotnet-sdk/commit/e74e8e7a58d90e46bbcd5d7e9433545412e07bbd)) +* **deps:** update github/codeql-action digest to b56ba49 ([#384](https://github.com/open-feature/dotnet-sdk/issues/384)) ([cc2990f](https://github.com/open-feature/dotnet-sdk/commit/cc2990ff8e7bf5148ab1cd867d9bfabfc0b7af8a)) +* **deps:** update spec digest to 0cd553d ([#389](https://github.com/open-feature/dotnet-sdk/issues/389)) ([85075ac](https://github.com/open-feature/dotnet-sdk/commit/85075ac7f46783dd1bcfdbbe6bd10d81eb9adb8a)) +* **deps:** update spec digest to 54952f3 ([#373](https://github.com/open-feature/dotnet-sdk/issues/373)) ([1e8b230](https://github.com/open-feature/dotnet-sdk/commit/1e8b2307369710ea0b5ae0e8a8f1f1293ea066dc)) +* **deps:** update spec digest to a69f748 ([#382](https://github.com/open-feature/dotnet-sdk/issues/382)) ([4977542](https://github.com/open-feature/dotnet-sdk/commit/4977542515bff302c7a88f3fa301bb129d7ea8cf)) +* remove FluentAssertions ([#361](https://github.com/open-feature/dotnet-sdk/issues/361)) ([4ecfd24](https://github.com/open-feature/dotnet-sdk/commit/4ecfd249181cf8fe372810a1fc3369347c6302fc)) +* Replace SpecFlow with Reqnroll for testing framework ([#368](https://github.com/open-feature/dotnet-sdk/issues/368)) ([ed6ee2c](https://github.com/open-feature/dotnet-sdk/commit/ed6ee2c502b16e49c91c6363ae6b3f54401a85cb)), closes [#354](https://github.com/open-feature/dotnet-sdk/issues/354) +* update release please repo, specify action permissions ([#369](https://github.com/open-feature/dotnet-sdk/issues/369)) ([63846ad](https://github.com/open-feature/dotnet-sdk/commit/63846ad1033399e9c84ad5946367c5eef2663b5b)) + + +### πŸ”„ Refactoring + +* Improve EventExecutor ([#393](https://github.com/open-feature/dotnet-sdk/issues/393)) ([46274a2](https://github.com/open-feature/dotnet-sdk/commit/46274a21d74b5cfffd4cfbc30e5e49e2dc1f256c)) + ## [2.3.1](https://github.com/open-feature/dotnet-sdk/compare/v2.3.0...v2.3.1) (2025-02-04) diff --git a/README.md b/README.md index b02f5721..fb6550bd 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v2.3.1&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.1) + ![Release](https://img.shields.io/static/v1?label=release&message=v2.3.2&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.2) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index db886413..c489d7f9 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.3.1 + 2.3.2 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 2bf1c1cc..f90b1afc 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.3.1 +2.3.2 From 4043d3d7610b398e6be035a0e1bf28e7c81ebf18 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 28 Mar 2025 16:20:50 +0800 Subject: [PATCH 282/316] refactor: simplify the InternalsVisibleTo usage (#408) ## This PR simplify and unify the InternalsVisibleTo usage --------- Signed-off-by: Weihan Li --- .../OpenFeature.DependencyInjection.csproj | 8 ++------ src/OpenFeature/FeatureProvider.cs | 3 --- src/OpenFeature/OpenFeature.csproj | 1 + 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 6f8163fb..99270ab3 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -17,12 +17,8 @@
- - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>DynamicProxyGenAssembly2 - + + diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 6aff0129..b5b9a30f 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,13 +1,10 @@ using System.Collections.Immutable; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // required to allow NSubstitute mocking of internal methods - namespace OpenFeature { /// diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index 357d39c5..c47b109d 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -12,6 +12,7 @@ + From 84ea288a3bc6e5ec8a797312f36e44c28d03c95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:14:49 +0100 Subject: [PATCH 283/316] docs: Update contributing guidelines (#413) 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> --- CONTRIBUTING.md | 148 +++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48fe03bc..98800faf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,17 +6,17 @@ You can contribute to this project from a Windows, macOS or Linux machine. On all platforms, the minimum requirements are: -* Git client and command line tools. -* .netstandard 2.0 or higher capable dotnet sdk (.Net Framework 4.6.2 or higher/.Net Core 3 or higher). +- Git client and command line tools. +- .netstandard 2.0 or higher capable dotnet sdk (.Net Framework 4.6.2 or higher/.Net 8 or higher). ### Linux or MacOS -* Jetbrains Rider 2022.2+ or Visual Studio 2022+ for Mac or Visual Studio Code +- JetBrains Rider 2022.2+ or Visual Studio 2022+ for Mac or Visual Studio Code ### Windows -* Jetbrains Rider 2022.2+ or Visual Studio 2022+ or Visual Studio Code -* .NET Framework 4.6.2+ +- JetBrains Rider 2022.2+ or Visual Studio 2022+ or Visual Studio Code +- .NET Framework 4.6.2+ ## Pull Request @@ -29,11 +29,13 @@ git clone https://github.com/open-feature/dotnet-sdk.git openfeature-dotnet-sdk ``` Navigate to the repository folder + ```bash cd openfeature-dotnet-sdk ``` Add your fork as an origin + ```bash git remote add fork https://github.com/YOUR_GITHUB_USERNAME/dotnet-sdk.git ``` @@ -61,13 +63,13 @@ dotnet test test/OpenFeature.Tests/ #### E2E tests -To be able to run the e2e tests, first we need to initialize the submodule and copy the test files: +To be able to run the e2e tests, first we need to initialize the submodule. ```bash -git submodule update --init --recursive && cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ +git submodule update --init --recursive ``` -Now you can run the tests using: +Since all the spec files are copied during the build process. Now you can run the tests using: ```bash dotnet test test/OpenFeature.E2ETests/ @@ -75,23 +77,23 @@ dotnet test test/OpenFeature.E2ETests/ ### How to Receive Comments -* If the PR is not ready for review, please mark it as - [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). -* Make sure all required CI checks are clear. -* Submit small, focused PRs addressing a single concern/issue. -* Make sure the PR title reflects the contribution. -* Write a summary that helps understand the change. -* Include usage examples in the summary, where applicable. +- If the PR is not ready for review, please mark it as + [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). +- Make sure all required CI checks are clear. +- Submit small, focused PRs addressing a single concern/issue. +- Make sure the PR title reflects the contribution. +- Write a summary that helps understand the change. +- Include usage examples in the summary, where applicable. ### How to Get PRs Merged A PR is considered to be **ready to merge** when: -* Major feedbacks are resolved. -* It has been open for review for at least one working day. This gives people - reasonable time to review. -* Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. -* Urgent fix can take exception as long as it has been actively communicated. +- Major feedbacks are resolved. +- It has been open for review for at least one working day. This gives people + reasonable time to review. +- Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. +- Urgent fix can take exception as long as it has been actively communicated. Any Maintainer can merge the PR once it is **ready to merge**. Note, that some PRs may not be merged immediately if the repo is in the process of a release and @@ -100,32 +102,33 @@ the maintainers decided to defer the PR to the next release train. If a PR has been stuck (e.g. there are lots of debates and people couldn't agree on each other), the owner should try to get people aligned by: -* Consolidating the perspectives and putting a summary in the PR. It is - recommended to add a link into the PR description, which points to a comment - with a summary in the PR conversation. -* Tagging subdomain experts (by looking at the change history) in the PR asking - for suggestion. -* Reaching out to more people on the [CNCF OpenFeature Slack channel](https://cloud-native.slack.com/archives/C0344AANLA1). -* Stepping back to see if it makes sense to narrow down the scope of the PR or - split it up. -* If none of the above worked and the PR has been stuck for more than 2 weeks, - the owner should bring it to the OpenFeatures [meeting](README.md#contributing). +- Consolidating the perspectives and putting a summary in the PR. It is + recommended to add a link into the PR description, which points to a comment + with a summary in the PR conversation. +- Tagging subdomain experts (by looking at the change history) in the PR asking + for suggestion. +- Reaching out to more people on the [CNCF OpenFeature Slack channel](https://cloud-native.slack.com/archives/C0344AANLA1). +- Stepping back to see if it makes sense to narrow down the scope of the PR or + split it up. +- If none of the above worked and the PR has been stuck for more than 2 weeks, + the owner should bring it to the OpenFeatures [meeting](README.md#contributing). ## Automated Changelog -Each time a release is published the changelogs will be generated automatically using [dotnet-releaser](https://github.com/xoofx/dotnet-releaser/blob/main/doc/changelog_user_guide.md#13-categories). The tool will organise the changes based on the PR labels. +Each time a release is published the changelogs will be generated automatically using [googleapis/release-please-action](https://github.com/googleapis/release-please-action). The tool will organise the changes based on the PR labels. +Please make sure you follow the latest [conventions](https://www.conventionalcommits.org/en/v1.0.0/). We use an automation to check if the pull request respects the desired conventions. You can check it [here](https://github.com/open-feature/dotnet-sdk/actions/workflows/lint-pr.yml). Must be one of the following: + +- build: Changes that affect the build system or external dependencies (example scopes: nuget) +- ci: Changes to our CI configuration files and scripts (example scopes: GitHub Actions, Coverage) +- docs: Documentation only changes +- feat: A new feature +- fix: A bug fix +- perf: A code change that improves performance +- refactor: A code change that neither fixes a bug nor adds a feature +- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +- test: Adding missing tests or correcting existing tests -- 🚨 Breaking Changes = `breaking-change` -- ✨ New Features = `feature` -- πŸ› Bug Fixes = `bug` -- πŸš€ Enhancements = `enhancement` -- 🧰 Maintenance = `maintenance` -- 🏭 Tests = `tests`, `test` -- πŸ›  Examples = `examples` -- πŸ“š Documentation = `documentation` -- 🌎 Accessibility = `translations` -- πŸ“¦ Dependencies = `dependencies` -- 🧰 Misc = `misc` +If you want to point out a breaking change, you should use `!` after the type. For example: `feat!: excellent new feature`. ## Design Choices @@ -153,34 +156,37 @@ dotnet release/OpenFeature.Benchmarks.dll ## Consuming pre-release packages 1. Acquire a [GitHub personal access token (PAT)](https://docs.github.com/github/authenticating-to-github/creating-a-personal-access-token) scoped for `read:packages` and verify the permissions: - ```console - $ gh auth login --scopes read:packages - - ? What account do you want to log into? GitHub.com - ? What is your preferred protocol for Git operations? HTTPS - ? How would you like to authenticate GitHub CLI? Login with a web browser - - ! First copy your one-time code: ****-**** - Press Enter to open github.com in your browser... - - βœ“ Authentication complete. - - gh config set -h github.com git_protocol https - βœ“ Configured git protocol - βœ“ Logged in as ******** - ``` - - ```console - $ gh auth status - - github.com - βœ“ Logged in to github.com as ******** (~/.config/gh/hosts.yml) - βœ“ Git operations for github.com configured to use https protocol. - βœ“ Token: gho_************************************ - βœ“ Token scopes: gist, read:org, read:packages, repo, workflow - ``` + + ```console + $ gh auth login --scopes read:packages + + ? What account do you want to log into? GitHub.com + ? What is your preferred protocol for Git operations? HTTPS + ? How would you like to authenticate GitHub CLI? Login with a web browser + + ! First copy your one-time code: ****-**** + Press Enter to open github.com in your browser... + + βœ“ Authentication complete. + - gh config set -h github.com git_protocol https + βœ“ Configured git protocol + βœ“ Logged in as ******** + ``` + + ```console + $ gh auth status + + github.com + βœ“ Logged in to github.com as ******** (~/.config/gh/hosts.yml) + βœ“ Git operations for github.com configured to use https protocol. + βœ“ Token: gho_************************************ + βœ“ Token scopes: gist, read:org, read:packages, repo, workflow + ``` + 2. Run the following command to configure your local environment to consume packages from GitHub Packages: - ```console - $ dotnet nuget update source github-open-feature --username $(gh api user --jq .email) --password $(gh auth token) --store-password-in-clear-text - Package source "github-open-feature" was successfully updated. - ``` + ```console + $ dotnet nuget update source github-open-feature --username $(gh api user --jq .email) --password $(gh auth token) --store-password-in-clear-text + + Package source "github-open-feature" was successfully updated. + ``` From 6c23f21d56ef6cc6adce7f798ee302924c227e1f Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Tue, 1 Apr 2025 18:18:04 +0800 Subject: [PATCH 284/316] feat: update FeatureLifecycleStateOptions.StopState default to Stopped (#414) ## This PR - feat: update FeatureLifecycleStateOptions.StopState default to Stopped - refactor: remove the default options setup since it's same as default value, no need to configure ### Related Issues Fixes #410 --------- Signed-off-by: Weihan Li --- .../FeatureLifecycleStateOptions.cs | 2 +- .../OpenFeatureBuilderExtensions.cs | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs index 91e3047d..4e3c1c33 100644 --- a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs +++ b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs @@ -14,5 +14,5 @@ public class FeatureLifecycleStateOptions /// /// Gets or sets the state during the feature shutdown lifecycle. /// - public FeatureStopState StopState { get; set; } = FeatureStopState.Stopping; + public FeatureStopState StopState { get; set; } = FeatureStopState.Stopped; } diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs index 16f437b3..80e760d9 100644 --- a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -19,15 +19,7 @@ public static partial class OpenFeatureBuilderExtensions /// The instance. public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null) { - if (configureOptions == null) - { - builder.Services.Configure(cfg => - { - cfg.StartState = FeatureStartState.Starting; - cfg.StopState = FeatureStopState.Stopping; - }); - } - else + if (configureOptions is not null) { builder.Services.Configure(configureOptions); } From 2bed467317ab0afa6d3e3718e89a5bb05453d649 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:25:39 +0100 Subject: [PATCH 285/316] chore(deps): update github/codeql-action digest to 45775bd (#419) 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://redirect.github.com/github/codeql-action) | action | digest | `1b549b9` -> `45775bd` | --- ### 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 5fc869ca..2e3a1e14 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@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # 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@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # 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@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3 From 65615d83b59bfd49241bb6bee27b76caae9542b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:52:35 +0100 Subject: [PATCH 286/316] ci: Add provenance attestation (#420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes several updates to the GitHub Actions workflows to enhance CI/CD capabilities and improve artifact security. The key changes involve adding new permissions and steps to the workflows for both continuous integration and release processes. Enhancements to CI workflow: * [`.github/workflows/ci.yml`](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL7-R11): Adjusted the indentation for `paths-ignore` in both `push` and `pull_request` triggers. * [`.github/workflows/ci.yml`](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fR53-R54): Added `id-token` and `attestations` permissions under `jobs`. * [`.github/workflows/ci.yml`](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fR96-R100): Introduced a step to generate artifact attestation using `actions/attest-build-provenance`. Enhancements to release workflow: * [`.github/workflows/release.yml`](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34R33-R36): Added `id-token`, `contents`, and `attestations` permissions under `jobs`. * [`.github/workflows/release.yml`](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34R63-R67): Added a step to generate artifact attestation using `actions/attest-build-provenance`. * [`.github/workflows/release.yml`](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L90-R99): Simplified the `run` command for attaching SBOM to the artifact. ### Related Issues Fixes #409 ### Notes The attestation for the PR can be checked here: https://github.com/open-feature/dotnet-sdk/attestations/6175280 --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/ci.yml | 17 ++++++++++++----- .github/workflows/release.yml | 12 ++++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06746aff..ffa94761 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,19 +2,19 @@ name: CI on: push: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" pull_request: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" jobs: build: strategy: matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} @@ -50,6 +50,8 @@ jobs: permissions: contents: read packages: write + id-token: write + attestations: write runs-on: ubuntu-latest @@ -91,3 +93,8 @@ jobs: with: name: nupkgs path: src/**/*.nupkg + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + with: + subject-path: "src/**/*.nupkg" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5b17a1b..14f3dae1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,10 @@ jobs: release: runs-on: ubuntu-latest needs: release-please + permissions: + id-token: write + contents: read + attestations: write if: ${{ needs.release-please.outputs.release_created }} steps: @@ -56,6 +60,11 @@ jobs: - name: Publish to Nuget run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.NUGET_TOKEN }}" --source https://api.nuget.org/v3/index.json + - name: Generate artifact attestation + uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + with: + subject-path: "src/**/*.nupkg" + sbom: runs-on: ubuntu-latest permissions: @@ -87,5 +96,4 @@ jobs: - name: Attach SBOM to artifact env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - run: - gh release upload ${{ needs.release-please.outputs.release_tag_name }} bom.json + run: gh release upload ${{ needs.release-please.outputs.release_tag_name }} bom.json From 3994a20f90e5c7e488bccaf4078d76d973ec9e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:33:09 +0100 Subject: [PATCH 287/316] ci: Remove artifact attestation step from workflow (#425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes a small change to the `.github/workflows/ci.yml` file. The change removes the step that generates artifact attestation using the `actions/attest-build-provenance` action. * Removed the `Generate artifact attestation` step from the CI workflow in `.github/workflows/ci.yml`. ### Related Issues Fixes #423 ### Notes This only removes execution from the PRs and forks. It will not be removed from the main publishing action. Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffa94761..ec684b70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,8 +93,3 @@ jobs: with: name: nupkgs path: src/**/*.nupkg - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 - with: - subject-path: "src/**/*.nupkg" From 3c7dca3a56af48bd0521b680ef3be9e6b9592a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:33:44 +0100 Subject: [PATCH 288/316] build: Change dependency assets (#426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes updates to the `Directory.Packages.props` file to centralize and manage package versions more effectively, particularly for Microsoft Extensions packages. Key changes include: * Added a new property `MicrosoftExtensionsVersion` with a default value of `8.0.0` and a conditional value of `9.0.0` for `net9.0` target framework. * Updated `PackageVersion` elements to use the new `MicrosoftExtensionsVersion` property instead of hardcoded version numbers. ### Related Issues Fixes #424 ### Notes I tried to follow the https://github.com/App-vNext/Polly approach to determining the dependencies we should bring in. It seems that all the major libraries depend on the lowest version of the target framework. For example, if the target is net 8.0, the package will be 8.0.0. If the target is net 9.0, the package will be 9.0.0. Thanks, @kylejuliandev, for the discussion and the examples in Slack. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c30681f6..6bdfa455 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,17 +2,21 @@ true + 8.0.0 + + + 9.0.0 - - - - - - - - + + + + + + + + @@ -35,4 +39,4 @@ - + \ No newline at end of file From b0b168ffc051e3a6c55f66ea6af4208e7d64419d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:01:13 +0100 Subject: [PATCH 289/316] fix: Refactor error handling and improve documentation (#417) 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 includes updates to the `README.md`, changes to error handling in `OpenFeatureClient`, and improvements to the `InMemoryProvider` and its related tests. Documentation updates: * Added notes and examples to the `README.md` to improve clarity on logging, transaction context propagators, dependency injection, and custom providers. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R4) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R20) [[3]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R157) [[4]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R170) [[5]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R266) [[6]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R276) [[7]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R360-R378) [[8]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R387-R390) [[9]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R406) [[10]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R450) [[11]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R469) Error handling improvements: * Removed redundant error logging methods `FlagEvaluationError` and `FlagEvaluationErrorWithDescription` in `OpenFeatureClient`. [[1]](diffhunk://#diff-c23c8a3ea4538fbdcf6b1cf93ea3de456906e4d267fc4b2ba3f8b1cb186a7907L279) [[2]](diffhunk://#diff-c23c8a3ea4538fbdcf6b1cf93ea3de456906e4d267fc4b2ba3f8b1cb186a7907L293) [[3]](diffhunk://#diff-c23c8a3ea4538fbdcf6b1cf93ea3de456906e4d267fc4b2ba3f8b1cb186a7907L400-L402) InMemoryProvider enhancements: * Changed `InMemoryProvider` to return `ResolutionDetails` with appropriate error types instead of throwing exceptions for missing flags or type mismatches. [[1]](diffhunk://#diff-4734ad108181b3c0b9f2c89e921b023e0d3e06d3c26ba1bed6352a75643469b0L106-R105) [[2]](diffhunk://#diff-4734ad108181b3c0b9f2c89e921b023e0d3e06d3c26ba1bed6352a75643469b0L116-R115) Test updates: * Updated tests in `OpenFeatureClientTests` and `InMemoryProviderTests` to reflect changes in error handling and ensure proper error types and reasons are returned. [[1]](diffhunk://#diff-97c93c206b866c1293c8c476783ad62d653cbac48716afc15d418b7701431e30L187-R187) [[2]](diffhunk://#diff-9cc800e07a869b42598d8f63786c01c406128056e28c995e73b13f229d9c912fL178-R185) [[3]](diffhunk://#diff-9cc800e07a869b42598d8f63786c01c406128056e28c995e73b13f229d9c912fL233-R242) Configuration changes: * Modified `global.json` to update the SDK roll-forward policy from `latestMajor` to `latestFeature`. ### Related Issues Fixes #416 ### Notes After evaluating the `global.json`, I noticed the [rollForward](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json#rollforward) was way too high for what we should be using. I reduced to `latestFeature` which is safer and would allow us to change the GitHub Actions to use the version from `global.json` instead of from declaring it in the action itself. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- README.md | 43 +++++++++++++------ global.json | 4 +- src/OpenFeature/OpenFeatureClient.cs | 5 --- .../Providers/Memory/InMemoryProvider.cs | 5 +-- .../OpenFeatureClientTests.cs | 2 +- .../Providers/Memory/InMemoryProviderTests.cs | 15 +++++-- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index fb6550bd..2ba47096 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ + ![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) ## .NET SDK @@ -9,13 +10,14 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ - ![Release](https://img.shields.io/static/v1?label=release&message=v2.3.2&color=blue&style=for-the-badge) +![Release](https://img.shields.io/static/v1?label=release&message=v2.3.2&color=blue&style=for-the-badge) ](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.2) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) [![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) + [OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. @@ -70,17 +72,17 @@ public async Task Example() | Status | Features | Description | | ------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| βœ… | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | -| βœ… | [Logging](#logging) | Integrate with popular logging packages. | -| βœ… | [Domains](#domains) | Logically bind clients with providers. | -| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| βœ… | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). | -| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | -| πŸ”¬ | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. | +| βœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| βœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| βœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| βœ… | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | +| βœ… | [Logging](#logging) | Integrate with popular logging packages. | +| βœ… | [Domains](#domains) | Logically bind clients with providers. | +| βœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| βœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| βœ… | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). | +| βœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| πŸ”¬ | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. | > Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌ | Experimental: πŸ”¬ @@ -152,6 +154,7 @@ var value = await client.GetBooleanValueAsync("boolFlag", false, context, new Fl ### Logging The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. +Note that in accordance with the OpenFeature specification, the SDK doesn't generally log messages during flag evaluation. If you need further troubleshooting, please look into the `Logging Hook` section. #### Logging Hook @@ -164,6 +167,7 @@ var logger = loggerFactory.CreateLogger("Program"); var client = Api.Instance.GetClient(); client.AddHooks(new LoggingHook(logger)); ``` + See [hooks](#hooks) for more information on configuring hooks. ### Domains @@ -259,6 +263,7 @@ To register a [AsyncLocal](https://learn.microsoft.com/en-us/dotnet/api/system.t // registering the AsyncLocalTransactionContextPropagator Api.Instance.SetTransactionContextPropagator(new AsyncLocalTransactionContextPropagator()); ``` + Once you've registered a transaction context propagator, you can propagate the data into request-scoped transaction context. ```csharp @@ -268,6 +273,7 @@ EvaluationContext transactionContext = EvaluationContext.Builder() .Build(); Api.Instance.SetTransactionContext(transactionContext); ``` + Additionally, you can develop a custom transaction context propagator by implementing the `TransactionContextPropagator` interface and registering it as shown above. ## Extending @@ -351,19 +357,25 @@ public class MyHook : Hook 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 + > [!NOTE] > The OpenFeature.DependencyInjection and OpenFeature.Hosting packages are currently experimental. They streamline the integration of OpenFeature within .NET applications, allowing for seamless configuration and lifecycle management of feature flag providers using dependency injection and hosting services. #### Installation + To set up dependency injection and hosting capabilities for OpenFeature, install the following packages: + ```sh dotnet add package OpenFeature.DependencyInjection dotnet add package OpenFeature.Hosting ``` + #### Usage Examples + For a basic configuration, you can use the InMemoryProvider. This provider is simple and well-suited for development and testing purposes. **Basic Configuration:** + ```csharp builder.Services.AddOpenFeature(featureBuilder => { featureBuilder @@ -372,8 +384,10 @@ builder.Services.AddOpenFeature(featureBuilder => { .AddInMemoryProvider(); }); ``` + **Domain-Scoped Provider Configuration:**
To set up multiple providers with a selection policy, define logic for choosing the default provider. This example designates `name1` as the default provider: + ```csharp builder.Services.AddOpenFeature(featureBuilder => { featureBuilder @@ -389,6 +403,7 @@ builder.Services.AddOpenFeature(featureBuilder => { ``` ### Registering a Custom Provider + You can register a custom provider, such as `InMemoryProvider`, with OpenFeature using the `AddProvider` method. This approach allows you to dynamically resolve services or configurations during registration. ```csharp @@ -406,7 +421,7 @@ services.AddOpenFeature(builder => // Register a custom provider, such as InMemoryProvider return new InMemoryProvider(flags); }); -}); +}); ``` #### Adding a Domain-Scoped Provider @@ -432,6 +447,7 @@ services.AddOpenFeature(builder => ``` + ## ⭐️ Support the project - Give this repo a ⭐️! @@ -450,4 +466,5 @@ Interested in contributing? Great, we'd love your help! To get started, take a l [![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) Made with [contrib.rocks](https://contrib.rocks). + diff --git a/global.json b/global.json index 46506cda..3018f657 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "rollForward": "latestMajor", + "rollForward": "latestFeature", "version": "9.0.202", "allowPrerelease": false } -} +} \ No newline at end of file diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 8c39621b..03420a2a 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -276,7 +276,6 @@ await this.TriggerAfterHooksAsync( else { var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage); - this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception); await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, options, cancellationToken) .ConfigureAwait(false); } @@ -290,7 +289,6 @@ await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, opti } catch (Exception ex) { - this.FlagEvaluationError(flagKey, 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); @@ -397,9 +395,6 @@ public void Track(string trackingEventName, EvaluationContext? evaluationContext [LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")] partial void HookReturnedNull(string hookName); - [LoggerMessage(101, LogLevel.Error, "Error while evaluating flag {FlagKey}")] - partial void FlagEvaluationError(string flagKey, Exception exception); - [LoggerMessage(102, LogLevel.Error, "Error while evaluating flag {FlagKey}: {ErrorType}")] partial void FlagEvaluationErrorWithDescription(string flagKey, string errorType, Exception exception); diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 4a06dc85..f95a0a06 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -15,7 +15,6 @@ namespace OpenFeature.Providers.Memory /// In Memory Provider specification public class InMemoryProvider : FeatureProvider { - private readonly Metadata _metadata = new Metadata("InMemory"); private Dictionary _flags; @@ -103,7 +102,7 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati { if (!this._flags.TryGetValue(flagKey, out var flag)) { - throw new FlagNotFoundException($"flag {flagKey} not found"); + return new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, Reason.Error); } // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. @@ -113,7 +112,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)}"); + throw new TypeMismatchException($"flag {flagKey} is not of type {typeof(T)}"); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index c16824cb..fc6f415f 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -184,7 +184,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc _ = mockedFeatureProvider.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); - mockedLogger.Received(1).IsEnabled(LogLevel.Error); + mockedLogger.Received(0).IsEnabled(LogLevel.Error); } [Fact] diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index c83ce0ce..7a174fc5 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -175,9 +175,14 @@ public async Task EmptyFlags_ShouldWork() } [Fact] - public async Task MissingFlag_ShouldThrow() + public async Task MissingFlag_ShouldReturnFlagNotFoundEvaluationFlag() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("missing-flag", false, EvaluationContext.Empty)); + // Act + var result = await this.commonProvider.ResolveBooleanValueAsync("missing-flag", false, EvaluationContext.Empty); + + // Assert + Assert.Equal(Reason.Error, result.Reason); + Assert.Equal(ErrorType.FlagNotFound, result.ErrorType); } [Fact] @@ -230,7 +235,11 @@ await provider.UpdateFlagsAsync(new Dictionary(){ var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res?.Type); - await Assert.ThrowsAsync(() => provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty)); + // old flag should be gone + var oldFlag = await provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty); + + Assert.Equal(Reason.Error, oldFlag.Reason); + Assert.Equal(ErrorType.FlagNotFound, oldFlag.ErrorType); // new flag should be present, old gone (defaults), handler run. ResolutionDetails detailsAfter = await provider.ResolveStringValueAsync("new-flag", "nope", EvaluationContext.Empty); From a2d4ca3cbe81668035b4cd34ba23df522d8ee710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:21:11 +0100 Subject: [PATCH 290/316] ci: Change actions to use global.json (#418) 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 includes several changes to the GitHub Actions workflows, primarily focusing on updating the .NET SDK setup and improving the release process. The most important changes are listed below: Updates to .NET SDK setup: * [`.github/workflows/ci.yml`](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL33-R33): Changed from specifying `dotnet-version` to using `global-json-file` for setting up the .NET SDK version. [[1]](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL33-R33) [[2]](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL68-R66) * [`.github/workflows/code-coverage.yml`](diffhunk://#diff-49708f979e226a1e7bd7a68d71b2e91aae8114dd3e9254d9830cd3b4d62d4303L31-R31): Updated to use `global-json-file` instead of `dotnet-version` for .NET SDK setup. * [`.github/workflows/dotnet-format.yml`](diffhunk://#diff-ca8c2611c79b991c0fbe04fec3c97c14dc83419f5efb1e8a7a96dd51e7df3e2aL20-R20): Modified to use `global-json-file` for .NET SDK setup. * [`.github/workflows/e2e.yml`](diffhunk://#diff-3e103440521ada06efd263ae09b259e5507e4b8f7408308dc227621ad9efa31eL26-R26): Changed to use `global-json-file` for setting up the .NET SDK version. * [`.github/workflows/release.yml`](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L45-R46): Updated to use `global-json-file` instead of `dotnet-version` for .NET SDK setup. [[1]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L45-R46) [[2]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L77-R76) Improvements to release process: * [`.github/workflows/release.yml`](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34R26): Added `release-type: simple` to the release configuration. * [`.github/workflows/release.yml`](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L90-R88): Simplified the command for attaching SBOM to the release artifact. Consistency improvements: * [`.github/workflows/ci.yml`](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL7-R11): Adjusted indentation for `paths-ignore` in `push` and `pull_request` sections. ### Notes - This ensures the version used in the action is the one declared on the `global.json`. - Fixed an issue in the please-release. The release type is a mandatory field. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++------ .github/workflows/code-coverage.yml | 4 +--- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 4 +--- .github/workflows/release.yml | 8 +++----- 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec684b70..0a26e59c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,7 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - dotnet-version: | - 8.0.x - 9.0.x + global-json-file: global.json source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Restore @@ -67,9 +65,7 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - dotnet-version: | - 8.0.x - 9.0.x + global-json-file: global.json source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Restore diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 603349e8..38313628 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -28,9 +28,7 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - dotnet-version: | - 8.0.x - 9.0.x + global-json-file: global.json source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Run Test diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index e96d5ec0..63259de0 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -17,7 +17,7 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: - dotnet-version: 9.0.x + global-json-file: global.json - name: dotnet format run: dotnet format --verify-no-changes OpenFeature.sln diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f4a3d93c..ce4bb634 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,9 +23,7 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - dotnet-version: | - 8.0.x - 9.0.x + global-json-file: global.json source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Initialize Tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14f3dae1..146c27e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}} default-branch: main signoff: "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>" + release-type: simple outputs: release_created: ${{ steps.release.outputs.release_created }} release_tag_name: ${{ steps.release.outputs.tag_name }} @@ -46,9 +47,7 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - dotnet-version: | - 8.0.x - 9.0.x + global-json-file: global.json source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install dependencies @@ -83,8 +82,7 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - dotnet-version: | - 9.0.x + global-json-file: global.json source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install CycloneDX.NET From 0c222cb5e90203e8f4740207d3dd82ec12179594 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 14 Apr 2025 13:52:31 -0400 Subject: [PATCH 291/316] chore: restrict publish to environment (#431) Restricts usage of the `NUGET_TOKEN` to only the `publish` environment. Signed-off-by: Todd Baert --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 146c27e3..a0fbad85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,7 @@ jobs: release_tag_name: ${{ steps.release.outputs.tag_name }} release: + environment: publish runs-on: ubuntu-latest needs: release-please permissions: From 7b06e14847f74f190b5cce481c3c4561fabd12d4 Mon Sep 17 00:00:00 2001 From: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:57:29 -0400 Subject: [PATCH 292/316] chore(main): release 2.4.0 (#412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.4.0](https://github.com/open-feature/dotnet-sdk/compare/v2.3.2...v2.4.0) (2025-04-14) ### πŸ› Bug Fixes * Refactor error handling and improve documentation ([#417](https://github.com/open-feature/dotnet-sdk/issues/417)) ([b0b168f](https://github.com/open-feature/dotnet-sdk/commit/b0b168ffc051e3a6c55f66ea6af4208e7d64419d)) ### ✨ New Features * update FeatureLifecycleStateOptions.StopState default to Stopped ([#414](https://github.com/open-feature/dotnet-sdk/issues/414)) ([6c23f21](https://github.com/open-feature/dotnet-sdk/commit/6c23f21d56ef6cc6adce7f798ee302924c227e1f)) ### 🧹 Chore * **deps:** update github/codeql-action digest to 45775bd ([#419](https://github.com/open-feature/dotnet-sdk/issues/419)) ([2bed467](https://github.com/open-feature/dotnet-sdk/commit/2bed467317ab0afa6d3e3718e89a5bb05453d649)) * restrict publish to environment ([#431](https://github.com/open-feature/dotnet-sdk/issues/431)) ([0c222cb](https://github.com/open-feature/dotnet-sdk/commit/0c222cb5e90203e8f4740207d3dd82ec12179594)) ### πŸ“š Documentation * Update contributing guidelines ([#413](https://github.com/open-feature/dotnet-sdk/issues/413)) ([84ea288](https://github.com/open-feature/dotnet-sdk/commit/84ea288a3bc6e5ec8a797312f36e44c28d03c95c)) ### πŸ”„ Refactoring * simplify the InternalsVisibleTo usage ([#408](https://github.com/open-feature/dotnet-sdk/issues/408)) ([4043d3d](https://github.com/open-feature/dotnet-sdk/commit/4043d3d7610b398e6be035a0e1bf28e7c81ebf18)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8d17a8e5..a549f59d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.2" + ".": "2.4.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9109cb..1219039f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [2.4.0](https://github.com/open-feature/dotnet-sdk/compare/v2.3.2...v2.4.0) (2025-04-14) + + +### πŸ› Bug Fixes + +* Refactor error handling and improve documentation ([#417](https://github.com/open-feature/dotnet-sdk/issues/417)) ([b0b168f](https://github.com/open-feature/dotnet-sdk/commit/b0b168ffc051e3a6c55f66ea6af4208e7d64419d)) + + +### ✨ New Features + +* update FeatureLifecycleStateOptions.StopState default to Stopped ([#414](https://github.com/open-feature/dotnet-sdk/issues/414)) ([6c23f21](https://github.com/open-feature/dotnet-sdk/commit/6c23f21d56ef6cc6adce7f798ee302924c227e1f)) + + +### 🧹 Chore + +* **deps:** update github/codeql-action digest to 45775bd ([#419](https://github.com/open-feature/dotnet-sdk/issues/419)) ([2bed467](https://github.com/open-feature/dotnet-sdk/commit/2bed467317ab0afa6d3e3718e89a5bb05453d649)) +* restrict publish to environment ([#431](https://github.com/open-feature/dotnet-sdk/issues/431)) ([0c222cb](https://github.com/open-feature/dotnet-sdk/commit/0c222cb5e90203e8f4740207d3dd82ec12179594)) + + +### πŸ“š Documentation + +* Update contributing guidelines ([#413](https://github.com/open-feature/dotnet-sdk/issues/413)) ([84ea288](https://github.com/open-feature/dotnet-sdk/commit/84ea288a3bc6e5ec8a797312f36e44c28d03c95c)) + + +### πŸ”„ Refactoring + +* simplify the InternalsVisibleTo usage ([#408](https://github.com/open-feature/dotnet-sdk/issues/408)) ([4043d3d](https://github.com/open-feature/dotnet-sdk/commit/4043d3d7610b398e6be035a0e1bf28e7c81ebf18)) + ## [2.3.2](https://github.com/open-feature/dotnet-sdk/compare/v2.3.1...v2.3.2) (2025-03-24) diff --git a/README.md b/README.md index 2ba47096..cf8101ab 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) [ -![Release](https://img.shields.io/static/v1?label=release&message=v2.3.2&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.2) +![Release](https://img.shields.io/static/v1?label=release&message=v2.4.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.4.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index c489d7f9..92b0ccb5 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.3.2 + 2.4.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index f90b1afc..197c4d5c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.3.2 +2.4.0 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 293/316] 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://redirect.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://redirect.github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v542) [Compare Source](https://redirect.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://redirect.github.com/codecov/codecov-action/blob/HEAD/CHANGELOG.md#v541) [Compare Source](https://redirect.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://redirect.github.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1807](https://redirect.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://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1803](https://redirect.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://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1797](https://redirect.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://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1798](https://redirect.github.com/codecov/codecov-action/pull/1798) - chore(release): wrapper -0.2.1 by [@​app/codecov-releaser-app](https://redirect.github.com/app/codecov-releaser-app) in [https://github.com/codecov/codecov-action/pull/1788](https://redirect.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://redirect.github.com/app/dependabot) in [https://github.com/codecov/codecov-action/pull/1786](https://redirect.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 38313628..be0a5412 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 294/316] 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 0cd553d8..27e4461b 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 1e8311ae..6b2bfebf 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 00000000..c9f454ac --- /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 00000000..40141e79 --- /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 b3380132..13a4e5a3 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 295/316] 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 27e4461b..18cde170 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 296/316] 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 2e3a1e14..57ca4641 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 297/316] 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 0a26e59c..bb1c7227 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 be0a5412..a33413d8 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 0241f80a..018589ea 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 63259de0..16799cf1 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 ce4bb634..ae0ca839 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 5dbb5688..f2307927 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 298/316] 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 cf8101ab..7a226177 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 00000000..5d56eb87 --- /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 00000000..8c1dbb51 --- /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 69f58bde..8d99a283 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 03420a2a..4a00aa44 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 00000000..3d6b787c --- /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 00000000..96cbaf72 --- /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 bbb4da3f..ae53f6db 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 299/316] 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 a0fbad85..7a17569f 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 300/316] 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 f95a0a06..2eec879d 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 7a174fc5..c575dc56 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 301/316] 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 7a17569f..3702d88b 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 302/316] 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://redirect.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 57ca4641..d943ef01 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 303/316] 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 7a226177..b3d6ae98 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 70321883..1f52a2a1 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 304/316] 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 18cde170..36944c68 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 305/316] 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 daaea0b5..151c402c 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 306/316] 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 7297b04d..f6763f4d 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 1f52a2a1..cc0161c1 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 0c58ec4d..319844b8 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 4660e41a..d36f3d96 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 3d3c9dc8..369c10b2 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 94a35d5b..d63db712 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 16dbd024..76033746 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 eac06c1e..bd0653b5 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 b2c43dc7..b0431ab7 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 b1a5b64a..d685bb4a 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 4580ff31..0f9da24c 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 ffea8ab1..881d0464 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 81ded456..57bcf271 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 894a583d..60ba5f25 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 b66201d7..5d2e3af1 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 71742413..488009f4 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 83ff0cf3..2df3b29f 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 a1c1ddbd..edb75780 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 fe10afb5..d5d7e72b 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 f38356ad..cf0d4f4a 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 b5b9a30f..9c9d9327 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 c1dbbe38..d38550ff 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 5d56eb87..ecfdfabd 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 8c1dbb51..c80b8613 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 a8d318b0..b8308167 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 114b66b3..bb1cd91e 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 f39b7f52..c14e6e4b 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 b98e6e9d..ffdc4eeb 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 304e4cd9..ed4f989a 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 30e2ffe0..3d85ba98 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 11283b4f..a08e2041 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 8bba0aef..a261a6b3 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 8d99a283..4abc773c 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 d7c972d7..44a059ef 100644 --- a/src/OpenFeature/Model/Metadata.cs +++ b/src/OpenFeature/Model/Metadata.cs @@ -1,22 +1,21 @@ -namespace OpenFeature.Model +namespace OpenFeature.Model; + +/// +/// metadata +/// +public class Metadata { /// - /// metadata + /// Gets name of instance /// - public class Metadata - { - /// - /// Gets name of instance - /// - public string? Name { get; } + public string? Name { get; } - /// - /// Initializes a new instance of the class. - /// - /// Name of instance - public Metadata(string? name) - { - this.Name = name; - } + /// + /// Initializes a new instance of the class. + /// + /// Name of instance + public Metadata(string? name) + { + this.Name = name; } } diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index bdae057e..1977edb6 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -1,46 +1,45 @@ using System.Collections.Generic; using OpenFeature.Constant; -namespace OpenFeature.Model +namespace OpenFeature.Model; + +/// +/// The EventHandlerDelegate is an implementation of an Event Handler +/// +public delegate void EventHandlerDelegate(ProviderEventPayload? eventDetails); + +/// +/// Contains the payload of an OpenFeature Event. +/// +public class ProviderEventPayload { /// - /// The EventHandlerDelegate is an implementation of an Event Handler + /// Name of the provider. + /// + public string? ProviderName { get; set; } + + /// + /// Type of the event + /// + public ProviderEventTypes Type { get; set; } + + /// + /// A message providing more information about the event. + /// + public string? Message { get; set; } + + /// + /// Optional error associated with the event. + /// + public ErrorType? ErrorType { get; set; } + + /// + /// A List of flags that have been changed. /// - public delegate void EventHandlerDelegate(ProviderEventPayload? eventDetails); + public List? FlagsChanged { get; set; } /// - /// Contains the payload of an OpenFeature Event. + /// Metadata information for the event. /// - public class ProviderEventPayload - { - /// - /// Name of the provider. - /// - public string? ProviderName { get; set; } - - /// - /// Type of the event - /// - public ProviderEventTypes Type { get; set; } - - /// - /// A message providing more information about the event. - /// - public string? Message { get; set; } - - /// - /// Optional error associated with the event. - /// - public ErrorType? ErrorType { get; set; } - - /// - /// A List of flags that have been changed. - /// - public List? FlagsChanged { get; set; } - - /// - /// Metadata information for the event. - /// - public ImmutableMetadata? EventMetadata { get; set; } - } + public ImmutableMetadata? EventMetadata { get; set; } } diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs index 78b907d2..a5c43aed 100644 --- a/src/OpenFeature/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -1,74 +1,73 @@ using OpenFeature.Constant; -namespace OpenFeature.Model +namespace OpenFeature.Model; + +/// +/// Defines the contract that the is required to return +/// Describes the details of the feature flag being evaluated +/// +/// Flag value type +/// +public sealed class ResolutionDetails { /// - /// Defines the contract that the is required to return - /// Describes the details of the feature flag being evaluated + /// Feature flag evaluated value /// - /// Flag value type - /// - public sealed class ResolutionDetails - { - /// - /// 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. - /// - public string? ErrorMessage { get; } + /// + /// Message containing additional details about an error. + /// + 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 ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string? reason = null, - string? variant = null, 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 ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string? reason = null, + string? variant = null, 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/Structure.cs b/src/OpenFeature/Model/Structure.cs index 47c66923..9807ec45 100644 --- a/src/OpenFeature/Model/Structure.cs +++ b/src/OpenFeature/Model/Structure.cs @@ -3,122 +3,121 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -namespace OpenFeature.Model +namespace OpenFeature.Model; + +/// +/// Structure represents a map of Values +/// +public sealed class Structure : IEnumerable> { + private readonly ImmutableDictionary _attributes; + + /// + /// Internal constructor for use by the builder. + /// + internal Structure(ImmutableDictionary attributes) + { + this._attributes = attributes; + } + + /// + /// Private constructor for creating an empty . + /// + private Structure() + { + this._attributes = ImmutableDictionary.Empty; + } + + /// + /// An empty structure. + /// + public static Structure Empty { get; } = new Structure(); + + /// + /// Creates a new structure with the supplied attributes + /// + /// + public Structure(IDictionary attributes) + { + this._attributes = ImmutableDictionary.CreateRange(attributes); + } + + /// + /// Gets the Value at the specified key + /// + /// The key of the value to be retrieved + /// + public Value GetValue(string key) => this._attributes[key]; + + /// + /// Bool indicating if the specified key exists in the structure + /// + /// The key of the value to be retrieved + /// indicating the presence of the key. + public bool ContainsKey(string key) => this._attributes.ContainsKey(key); + + /// + /// Gets the value associated with the specified key by mutating the supplied value. + /// + /// The key of the value to be retrieved + /// value to be mutated + /// indicating the presence of the key. + public bool TryGetValue(string key, out Value? value) => this._attributes.TryGetValue(key, out value); + + /// + /// Gets all values as a Dictionary + /// + /// New representation of this Structure + public IImmutableDictionary AsDictionary() + { + return this._attributes; + } + + /// + /// Return the value at the supplied index + /// + /// The key of the value to be retrieved + public Value this[string key] + { + get => this._attributes[key]; + } + + /// + /// Return a list containing all the keys in this structure + /// + public IImmutableList Keys => this._attributes.Keys.ToImmutableList(); + /// - /// Structure represents a map of Values + /// Return an enumerable containing all the values in this structure /// - public sealed class Structure : IEnumerable> + public IImmutableList Values => this._attributes.Values.ToImmutableList(); + + /// + /// Return a count of all values + /// + public int Count => this._attributes.Count; + + /// + /// Return an enumerator for all values + /// + /// + public IEnumerator> GetEnumerator() + { + return this._attributes.GetEnumerator(); + } + + /// + /// Get a builder which can build a . + /// + /// The builder + public static StructureBuilder Builder() + { + return new StructureBuilder(); + } + + [ExcludeFromCodeCoverage] + IEnumerator IEnumerable.GetEnumerator() { - private readonly ImmutableDictionary _attributes; - - /// - /// Internal constructor for use by the builder. - /// - internal Structure(ImmutableDictionary attributes) - { - this._attributes = attributes; - } - - /// - /// Private constructor for creating an empty . - /// - private Structure() - { - this._attributes = ImmutableDictionary.Empty; - } - - /// - /// An empty structure. - /// - public static Structure Empty { get; } = new Structure(); - - /// - /// Creates a new structure with the supplied attributes - /// - /// - public Structure(IDictionary attributes) - { - this._attributes = ImmutableDictionary.CreateRange(attributes); - } - - /// - /// Gets the Value at the specified key - /// - /// The key of the value to be retrieved - /// - public Value GetValue(string key) => this._attributes[key]; - - /// - /// Bool indicating if the specified key exists in the structure - /// - /// The key of the value to be retrieved - /// indicating the presence of the key. - public bool ContainsKey(string key) => this._attributes.ContainsKey(key); - - /// - /// Gets the value associated with the specified key by mutating the supplied value. - /// - /// The key of the value to be retrieved - /// value to be mutated - /// indicating the presence of the key. - public bool TryGetValue(string key, out Value? value) => this._attributes.TryGetValue(key, out value); - - /// - /// Gets all values as a Dictionary - /// - /// New representation of this Structure - public IImmutableDictionary AsDictionary() - { - return this._attributes; - } - - /// - /// Return the value at the supplied index - /// - /// The key of the value to be retrieved - public Value this[string key] - { - get => this._attributes[key]; - } - - /// - /// Return a list containing all the keys in this structure - /// - public IImmutableList Keys => this._attributes.Keys.ToImmutableList(); - - /// - /// Return an enumerable containing all the values in this structure - /// - public IImmutableList Values => this._attributes.Values.ToImmutableList(); - - /// - /// Return a count of all values - /// - public int Count => this._attributes.Count; - - /// - /// Return an enumerator for all values - /// - /// - public IEnumerator> GetEnumerator() - { - return this._attributes.GetEnumerator(); - } - - /// - /// Get a builder which can build a . - /// - /// The builder - public static StructureBuilder Builder() - { - return new StructureBuilder(); - } - - [ExcludeFromCodeCoverage] - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } + return this.GetEnumerator(); } } diff --git a/src/OpenFeature/Model/StructureBuilder.cs b/src/OpenFeature/Model/StructureBuilder.cs index 4c44813d..0cc922ac 100644 --- a/src/OpenFeature/Model/StructureBuilder.cs +++ b/src/OpenFeature/Model/StructureBuilder.cs @@ -2,143 +2,142 @@ using System.Collections.Generic; using System.Collections.Immutable; -namespace OpenFeature.Model +namespace OpenFeature.Model; + +/// +/// A builder which allows the specification of attributes for a . +/// +/// A object is intended for use by a single thread and should not be used from +/// multiple threads. Once a has been created it is immutable and safe for use from +/// multiple threads. +/// +/// +public sealed class StructureBuilder { + private readonly ImmutableDictionary.Builder _attributes = + ImmutableDictionary.CreateBuilder(); + /// - /// A builder which allows the specification of attributes for a . - /// - /// A object is intended for use by a single thread and should not be used from - /// multiple threads. Once a has been created it is immutable and safe for use from - /// multiple threads. - /// + /// Internal to only allow direct creation by . /// - public sealed class StructureBuilder - { - private readonly ImmutableDictionary.Builder _attributes = - ImmutableDictionary.CreateBuilder(); + internal StructureBuilder() { } - /// - /// Internal to only allow direct creation by . - /// - internal StructureBuilder() { } - - /// - /// Set the key to the given . - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, Value value) - { - // Remove the attribute. Will not throw an exception if not present. - this._attributes.Remove(key); - this._attributes.Add(key, value); - return this; - } + /// + /// Set the key to the given . + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, Value value) + { + // Remove the attribute. Will not throw an exception if not present. + this._attributes.Remove(key); + this._attributes.Add(key, value); + return this; + } - /// - /// Set the key to the given string. - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, string value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given string. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, string value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Set the key to the given int. - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, int value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given int. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, int value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Set the key to the given double. - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, double value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given double. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, double value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Set the key to the given long. - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, long value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given long. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, long value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Set the key to the given bool. - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, bool value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given bool. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, bool value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Set the key to the given . - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, Structure value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given . + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, Structure value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Set the key to the given DateTime. - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, DateTime value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given DateTime. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, DateTime value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Set the key to the given list. - /// - /// The key for the value - /// The value to set - /// This builder - public StructureBuilder Set(string key, IList value) - { - this.Set(key, new Value(value)); - return this; - } + /// + /// Set the key to the given list. + /// + /// The key for the value + /// The value to set + /// This builder + public StructureBuilder Set(string key, IList value) + { + this.Set(key, new Value(value)); + return this; + } - /// - /// Build an immutable / - /// - /// The built - public Structure Build() - { - return new Structure(this._attributes.ToImmutable()); - } + /// + /// Build an immutable / + /// + /// The built + public Structure Build() + { + return new Structure(this._attributes.ToImmutable()); } } diff --git a/src/OpenFeature/Model/TrackingEventDetailsBuilder.cs b/src/OpenFeature/Model/TrackingEventDetailsBuilder.cs index 99a9d677..6520ab3e 100644 --- a/src/OpenFeature/Model/TrackingEventDetailsBuilder.cs +++ b/src/OpenFeature/Model/TrackingEventDetailsBuilder.cs @@ -1,159 +1,158 @@ 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 TrackingEventDetailsBuilder { + private readonly StructureBuilder _attributes = Structure.Builder(); + private double? _value; + /// - /// 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 TrackingEventDetailsBuilder - { - private readonly StructureBuilder _attributes = Structure.Builder(); - private double? _value; + internal TrackingEventDetailsBuilder() { } - /// - /// Internal to only allow direct creation by . - /// - internal TrackingEventDetailsBuilder() { } + /// + /// Set the predefined value field for the tracking details. + /// + /// + /// + public TrackingEventDetailsBuilder SetValue(double? value) + { + this._value = value; + return this; + } - /// - /// Set the predefined value field for the tracking details. - /// - /// - /// - public TrackingEventDetailsBuilder SetValue(double? value) - { - this._value = value; - return this; - } + /// + /// Set the key to the given . + /// + /// The key for the value + /// The value to set + /// This builder + public TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder 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 TrackingEventDetailsBuilder Set(string key, DateTime value) + /// + /// Incorporate existing tracking details into the builder. + /// + /// Any existing keys in the builder will be replaced by keys in the tracking details, including the Value set + /// through . + /// + /// + /// The tracking details to add merge + /// This builder + public TrackingEventDetailsBuilder Merge(TrackingEventDetails trackingDetails) + { + this._value = trackingDetails.Value; + foreach (var kvp in trackingDetails) { - this._attributes.Set(key, value); - return this; + this.Set(kvp.Key, kvp.Value); } - /// - /// Incorporate existing tracking details into the builder. - /// - /// Any existing keys in the builder will be replaced by keys in the tracking details, including the Value set - /// through . - /// - /// - /// The tracking details to add merge - /// This builder - public TrackingEventDetailsBuilder Merge(TrackingEventDetails trackingDetails) - { - this._value = trackingDetails.Value; - foreach (var kvp in trackingDetails) - { - this.Set(kvp.Key, kvp.Value); - } - - return this; - } + return this; + } - /// - /// Build an immutable . - /// - /// An immutable - public TrackingEventDetails Build() - { - return new TrackingEventDetails(this._attributes.Build(), this._value); - } + /// + /// Build an immutable . + /// + /// An immutable + public TrackingEventDetails Build() + { + return new TrackingEventDetails(this._attributes.Build(), this._value); } } diff --git a/src/OpenFeature/Model/Value.cs b/src/OpenFeature/Model/Value.cs index 88fb0734..2f75eca3 100644 --- a/src/OpenFeature/Model/Value.cs +++ b/src/OpenFeature/Model/Value.cs @@ -2,189 +2,188 @@ using System.Collections.Generic; using System.Collections.Immutable; -namespace OpenFeature.Model +namespace OpenFeature.Model; + +/// +/// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format. +/// This intermediate representation provides a good medium of exchange. +/// +public sealed class Value { + private readonly object? _innerValue; + + /// + /// Creates a Value with the inner value set to null + /// + public Value() => this._innerValue = null; + /// - /// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format. - /// This intermediate representation provides a good medium of exchange. + /// Creates a Value with the inner set to the object /// - public sealed class Value + /// The object to set as the inner value + public Value(Object value) { - private readonly object? _innerValue; - - /// - /// Creates a Value with the inner value set to null - /// - public Value() => this._innerValue = null; - - /// - /// Creates a Value with the inner set to the object - /// - /// The object to set as the inner value - public Value(Object value) + if (value is IList list) { - if (value is IList list) - { - value = list.ToImmutableList(); - } - // integer is a special case, convert those. - this._innerValue = value is int ? Convert.ToDouble(value) : value; - if (!(this.IsNull - || this.IsBoolean - || this.IsString - || this.IsNumber - || this.IsStructure - || this.IsList - || this.IsDateTime)) - { - throw new ArgumentException("Invalid value type: " + value.GetType()); - } + value = list.ToImmutableList(); } + // integer is a special case, convert those. + this._innerValue = value is int ? Convert.ToDouble(value) : value; + if (!(this.IsNull + || this.IsBoolean + || this.IsString + || this.IsNumber + || this.IsStructure + || this.IsList + || this.IsDateTime)) + { + throw new ArgumentException("Invalid value type: " + value.GetType()); + } + } - /// - /// Creates a Value with the inner value to the inner value of the value param - /// - /// Value type - public Value(Value value) => this._innerValue = value._innerValue; - - /// - /// Creates a Value with the inner set to bool type - /// - /// Bool type - public Value(bool value) => this._innerValue = value; - - /// - /// Creates a Value by converting value to a double - /// - /// Int type - public Value(int value) => this._innerValue = Convert.ToDouble(value); - - /// - /// Creates a Value with the inner set to double type - /// - /// Double type - public Value(double value) => this._innerValue = value; - - /// - /// Creates a Value with the inner set to string type - /// - /// String type - public Value(string value) => this._innerValue = value; - - /// - /// Creates a Value with the inner set to structure type - /// - /// Structure type - public Value(Structure value) => this._innerValue = value; - - /// - /// Creates a Value with the inner set to list type - /// - /// List type - public Value(IList value) => this._innerValue = value.ToImmutableList(); - - /// - /// Creates a Value with the inner set to DateTime type - /// - /// DateTime type - public Value(DateTime value) => this._innerValue = value; - - /// - /// Determines if inner value is null - /// - /// True if value is null - public bool IsNull => this._innerValue is null; - - /// - /// Determines if inner value is bool - /// - /// True if value is bool - public bool IsBoolean => this._innerValue is bool; - - /// - /// Determines if inner value is numeric - /// - /// True if value is double - public bool IsNumber => this._innerValue is double; - - /// - /// Determines if inner value is string - /// - /// True if value is string - public bool IsString => this._innerValue is string; - - /// - /// Determines if inner value is Structure - /// - /// True if value is Structure - public bool IsStructure => this._innerValue is Structure; - - /// - /// Determines if inner value is list - /// - /// True if value is list - public bool IsList => this._innerValue is IImmutableList; - - /// - /// Determines if inner value is DateTime - /// - /// True if value is DateTime - public bool IsDateTime => this._innerValue is DateTime; - - /// - /// Returns the underlying inner value as an object. Returns null if the inner value is null. - /// - /// Value as object - public object? AsObject => this._innerValue; - - /// - /// Returns the underlying int value. - /// Value will be null if it isn't an integer - /// - /// Value as int - public int? AsInteger => this.IsNumber ? Convert.ToInt32((double?)this._innerValue) : null; - - /// - /// Returns the underlying bool value. - /// Value will be null if it isn't a bool - /// - /// Value as bool - public bool? AsBoolean => this.IsBoolean ? (bool?)this._innerValue : null; - - /// - /// Returns the underlying double value. - /// Value will be null if it isn't a double - /// - /// Value as int - public double? AsDouble => this.IsNumber ? (double?)this._innerValue : null; - - /// - /// Returns the underlying string value. - /// Value will be null if it isn't a string - /// - /// Value as string - public string? AsString => this.IsString ? (string?)this._innerValue : null; - - /// - /// Returns the underlying Structure value. - /// Value will be null if it isn't a Structure - /// - /// Value as Structure - public Structure? AsStructure => this.IsStructure ? (Structure?)this._innerValue : null; - - /// - /// Returns the underlying List value. - /// Value will be null if it isn't a List - /// - /// Value as List - public IImmutableList? AsList => this.IsList ? (IImmutableList?)this._innerValue : null; - - /// - /// Returns the underlying DateTime value. - /// Value will be null if it isn't a DateTime - /// - /// Value as DateTime - public DateTime? AsDateTime => this.IsDateTime ? (DateTime?)this._innerValue : null; - } + /// + /// Creates a Value with the inner value to the inner value of the value param + /// + /// Value type + public Value(Value value) => this._innerValue = value._innerValue; + + /// + /// Creates a Value with the inner set to bool type + /// + /// Bool type + public Value(bool value) => this._innerValue = value; + + /// + /// Creates a Value by converting value to a double + /// + /// Int type + public Value(int value) => this._innerValue = Convert.ToDouble(value); + + /// + /// Creates a Value with the inner set to double type + /// + /// Double type + public Value(double value) => this._innerValue = value; + + /// + /// Creates a Value with the inner set to string type + /// + /// String type + public Value(string value) => this._innerValue = value; + + /// + /// Creates a Value with the inner set to structure type + /// + /// Structure type + public Value(Structure value) => this._innerValue = value; + + /// + /// Creates a Value with the inner set to list type + /// + /// List type + public Value(IList value) => this._innerValue = value.ToImmutableList(); + + /// + /// Creates a Value with the inner set to DateTime type + /// + /// DateTime type + public Value(DateTime value) => this._innerValue = value; + + /// + /// Determines if inner value is null + /// + /// True if value is null + public bool IsNull => this._innerValue is null; + + /// + /// Determines if inner value is bool + /// + /// True if value is bool + public bool IsBoolean => this._innerValue is bool; + + /// + /// Determines if inner value is numeric + /// + /// True if value is double + public bool IsNumber => this._innerValue is double; + + /// + /// Determines if inner value is string + /// + /// True if value is string + public bool IsString => this._innerValue is string; + + /// + /// Determines if inner value is Structure + /// + /// True if value is Structure + public bool IsStructure => this._innerValue is Structure; + + /// + /// Determines if inner value is list + /// + /// True if value is list + public bool IsList => this._innerValue is IImmutableList; + + /// + /// Determines if inner value is DateTime + /// + /// True if value is DateTime + public bool IsDateTime => this._innerValue is DateTime; + + /// + /// Returns the underlying inner value as an object. Returns null if the inner value is null. + /// + /// Value as object + public object? AsObject => this._innerValue; + + /// + /// Returns the underlying int value. + /// Value will be null if it isn't an integer + /// + /// Value as int + public int? AsInteger => this.IsNumber ? Convert.ToInt32((double?)this._innerValue) : null; + + /// + /// Returns the underlying bool value. + /// Value will be null if it isn't a bool + /// + /// Value as bool + public bool? AsBoolean => this.IsBoolean ? (bool?)this._innerValue : null; + + /// + /// Returns the underlying double value. + /// Value will be null if it isn't a double + /// + /// Value as int + public double? AsDouble => this.IsNumber ? (double?)this._innerValue : null; + + /// + /// Returns the underlying string value. + /// Value will be null if it isn't a string + /// + /// Value as string + public string? AsString => this.IsString ? (string?)this._innerValue : null; + + /// + /// Returns the underlying Structure value. + /// Value will be null if it isn't a Structure + /// + /// Value as Structure + public Structure? AsStructure => this.IsStructure ? (Structure?)this._innerValue : null; + + /// + /// Returns the underlying List value. + /// Value will be null if it isn't a List + /// + /// Value as List + public IImmutableList? AsList => this.IsList ? (IImmutableList?)this._innerValue : null; + + /// + /// Returns the underlying DateTime value. + /// Value will be null if it isn't a DateTime + /// + /// Value as DateTime + public DateTime? AsDateTime => this.IsDateTime ? (DateTime?)this._innerValue : null; } diff --git a/src/OpenFeature/NoOpProvider.cs b/src/OpenFeature/NoOpProvider.cs index 5d7b9caa..20973365 100644 --- a/src/OpenFeature/NoOpProvider.cs +++ b/src/OpenFeature/NoOpProvider.cs @@ -3,50 +3,49 @@ using OpenFeature.Constant; using OpenFeature.Model; -namespace OpenFeature +namespace OpenFeature; + +internal sealed class NoOpFeatureProvider : FeatureProvider { - internal sealed class NoOpFeatureProvider : FeatureProvider + private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); + + public override Metadata GetMetadata() + { + return this._metadata; + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue) { - private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); - - public override Metadata GetMetadata() - { - return this._metadata; - } - - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(NoOpResponse(flagKey, defaultValue)); - } - - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(NoOpResponse(flagKey, defaultValue)); - } - - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(NoOpResponse(flagKey, defaultValue)); - } - - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(NoOpResponse(flagKey, defaultValue)); - } - - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(NoOpResponse(flagKey, defaultValue)); - } - - private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue) - { - return new ResolutionDetails( - flagKey, - defaultValue, - reason: NoOpProvider.ReasonNoOp, - variant: NoOpProvider.Variant - ); - } + return new ResolutionDetails( + flagKey, + defaultValue, + reason: NoOpProvider.ReasonNoOp, + variant: NoOpProvider.Variant + ); } } diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 4a00aa44..98aae19f 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -12,333 +12,332 @@ using OpenFeature.Extension; using OpenFeature.Model; -namespace OpenFeature +namespace OpenFeature; + +/// +/// +/// +public sealed partial class FeatureClient : IFeatureClient { + private readonly ClientMetadata _metadata; + private readonly ConcurrentStack _hooks = new ConcurrentStack(); + private readonly ILogger _logger; + private readonly Func _providerAccessor; + private EvaluationContext _evaluationContext; + + private readonly object _evaluationContextLock = new object(); + /// - /// + /// Get a provider and an associated typed flag resolution method. + /// + /// The global provider could change between two accesses, so in order to safely get provider information we + /// must first alias it and then use that alias to access everything we need. + /// /// - public sealed partial class FeatureClient : IFeatureClient + /// + /// This method should return the desired flag resolution method from the given provider reference. + /// + /// The type of the resolution method + /// A tuple containing a resolution method and the provider it came from. + private (Func>>, FeatureProvider) + ExtractProvider( + Func>>> method) { - private readonly ClientMetadata _metadata; - private readonly ConcurrentStack _hooks = new ConcurrentStack(); - private readonly ILogger _logger; - private readonly Func _providerAccessor; - private EvaluationContext _evaluationContext; - - private readonly object _evaluationContextLock = new object(); - - /// - /// Get a provider and an associated typed flag resolution method. - /// - /// The global provider could change between two accesses, so in order to safely get provider information we - /// must first alias it and then use that alias to access everything we need. - /// - /// - /// - /// This method should return the desired flag resolution method from the given provider reference. - /// - /// The type of the resolution method - /// A tuple containing a resolution method and the provider it came from. - private (Func>>, FeatureProvider) - ExtractProvider( - Func>>> method) - { - // Alias the provider reference so getting the method and returning the provider are - // guaranteed to be the same object. - var provider = Api.Instance.GetProvider(this._metadata.Name!); + // Alias the provider reference so getting the method and returning the provider are + // guaranteed to be the same object. + var provider = Api.Instance.GetProvider(this._metadata.Name!); - return (method(provider), provider); - } + return (method(provider), provider); + } - /// - public ProviderStatus ProviderStatus => this._providerAccessor.Invoke().Status; + /// + public ProviderStatus ProviderStatus => this._providerAccessor.Invoke().Status; - /// - public EvaluationContext GetContext() - { - lock (this._evaluationContextLock) - { - return this._evaluationContext; - } - } - - /// - public void SetContext(EvaluationContext? context) + /// + public EvaluationContext GetContext() + { + lock (this._evaluationContextLock) { - lock (this._evaluationContextLock) - { - this._evaluationContext = context ?? EvaluationContext.Empty; - } + return this._evaluationContext; } + } - /// - /// Initializes a new instance of the class. - /// - /// Function to retrieve current provider - /// Name of client - /// Version of client - /// Logger used by client - /// Context given to this client - /// Throws if any of the required parameters are null - internal FeatureClient(Func providerAccessor, string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) + /// + public void SetContext(EvaluationContext? context) + { + lock (this._evaluationContextLock) { - this._metadata = new ClientMetadata(name, version); - this._logger = logger ?? NullLogger.Instance; this._evaluationContext = context ?? EvaluationContext.Empty; - this._providerAccessor = providerAccessor; } + } - /// - public ClientMetadata GetMetadata() => this._metadata; - - /// - /// Add hook to client - /// - /// 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); - - /// - public void AddHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) - { - Api.Instance.AddClientHandler(this._metadata.Name!, eventType, handler); - } + /// + /// Initializes a new instance of the class. + /// + /// Function to retrieve current provider + /// Name of client + /// Version of client + /// Logger used by client + /// Context given to this client + /// Throws if any of the required parameters are null + internal FeatureClient(Func providerAccessor, string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) + { + this._metadata = new ClientMetadata(name, version); + this._logger = logger ?? NullLogger.Instance; + this._evaluationContext = context ?? EvaluationContext.Empty; + this._providerAccessor = providerAccessor; + } - /// - public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) - { - Api.Instance.RemoveClientHandler(this._metadata.Name!, type, handler); - } + /// + public ClientMetadata GetMetadata() => this._metadata; - /// - 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); + /// + /// Add hook to client + /// + /// 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); - return; - } + /// + public void AddHandler(ProviderEventTypes eventType, EventHandlerDelegate handler) + { + Api.Instance.AddClientHandler(this._metadata.Name!, eventType, handler); + } - array = hooks.ToArray(); + /// + public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) + { + Api.Instance.RemoveClientHandler(this._metadata.Name!, type, handler); + } + /// + 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 - /// - public IEnumerable GetHooks() => this._hooks.Reverse(); - - /// - /// Removes all hooks from the client - /// - public void ClearHooks() => this._hooks.Clear(); - - /// - public async Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - (await this.GetBooleanDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; - - /// - public async Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveBooleanValueAsync), - FlagValueType.Boolean, flagKey, - defaultValue, context, config, cancellationToken).ConfigureAwait(false); - - /// - public async Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - (await this.GetStringDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; - - /// - public async Task> GetStringDetailsAsync(string flagKey, string defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStringValueAsync), - FlagValueType.String, flagKey, - defaultValue, context, config, cancellationToken).ConfigureAwait(false); - - /// - public async Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - (await this.GetIntegerDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; - - /// - public async Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveIntegerValueAsync), - FlagValueType.Number, flagKey, - defaultValue, context, config, cancellationToken).ConfigureAwait(false); - - /// - public async Task GetDoubleValueAsync(string flagKey, double defaultValue, - EvaluationContext? context = null, - FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - (await this.GetDoubleDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; - - /// - public async Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveDoubleValueAsync), - FlagValueType.Number, flagKey, - defaultValue, context, config, cancellationToken).ConfigureAwait(false); - - /// - public async Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - (await this.GetObjectDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; - - /// - public async Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, - EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => - await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStructureValueAsync), - FlagValueType.Object, flagKey, - defaultValue, context, config, cancellationToken).ConfigureAwait(false); - - private async Task> EvaluateFlagAsync( - (Func>>, FeatureProvider) providerInfo, - FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext? context = null, - FlagEvaluationOptions? options = null, - CancellationToken cancellationToken = default) + /// + public IEnumerable GetHooks() => this._hooks.Reverse(); + + /// + /// Removes all hooks from the client + /// + public void ClearHooks() => this._hooks.Clear(); + + /// + public async Task GetBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetBooleanDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; + + /// + public async Task> GetBooleanDetailsAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveBooleanValueAsync), + FlagValueType.Boolean, flagKey, + defaultValue, context, config, cancellationToken).ConfigureAwait(false); + + /// + public async Task GetStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetStringDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; + + /// + public async Task> GetStringDetailsAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStringValueAsync), + FlagValueType.String, flagKey, + defaultValue, context, config, cancellationToken).ConfigureAwait(false); + + /// + public async Task GetIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetIntegerDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; + + /// + public async Task> GetIntegerDetailsAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveIntegerValueAsync), + FlagValueType.Number, flagKey, + defaultValue, context, config, cancellationToken).ConfigureAwait(false); + + /// + public async Task GetDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetDoubleDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; + + /// + public async Task> GetDoubleDetailsAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveDoubleValueAsync), + FlagValueType.Number, flagKey, + defaultValue, context, config, cancellationToken).ConfigureAwait(false); + + /// + public async Task GetObjectValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + (await this.GetObjectDetailsAsync(flagKey, defaultValue, context, config, cancellationToken).ConfigureAwait(false)).Value; + + /// + public async Task> GetObjectDetailsAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, FlagEvaluationOptions? config = null, CancellationToken cancellationToken = default) => + await this.EvaluateFlagAsync(this.ExtractProvider(provider => provider.ResolveStructureValueAsync), + FlagValueType.Object, flagKey, + defaultValue, context, config, cancellationToken).ConfigureAwait(false); + + private async Task> EvaluateFlagAsync( + (Func>>, FeatureProvider) providerInfo, + FlagValueType flagValueType, string flagKey, T defaultValue, EvaluationContext? context = null, + FlagEvaluationOptions? options = null, + CancellationToken cancellationToken = default) + { + var resolveValueDelegate = providerInfo.Item1; + var provider = providerInfo.Item2; + + // New up an evaluation context if one was not provided. + context ??= EvaluationContext.Empty; + + // merge api, client, transaction and invocation context + var evaluationContextBuilder = EvaluationContext.Builder(); + evaluationContextBuilder.Merge(Api.Instance.GetContext()); // API context + evaluationContextBuilder.Merge(this.GetContext()); // Client context + evaluationContextBuilder.Merge(Api.Instance.GetTransactionContext()); // Transaction context + evaluationContextBuilder.Merge(context); // Invocation context + + var allHooks = ImmutableList.CreateBuilder() + .Concat(Api.Instance.GetHooks()) + .Concat(this.GetHooks()) + .Concat(options?.Hooks ?? Enumerable.Empty()) + .Concat(provider.GetProviderHooks()) + .ToImmutableList(); + + var sharedHookContext = new SharedHookContext( + flagKey, + defaultValue, + flagValueType, + this._metadata, + provider.GetMetadata() + ); + + FlagEvaluationDetails? evaluation = null; + var hookRunner = new HookRunner(allHooks, evaluationContextBuilder.Build(), sharedHookContext, + this._logger); + + try { - var resolveValueDelegate = providerInfo.Item1; - var provider = providerInfo.Item2; - - // New up an evaluation context if one was not provided. - context ??= EvaluationContext.Empty; - - // merge api, client, transaction and invocation context - var evaluationContextBuilder = EvaluationContext.Builder(); - evaluationContextBuilder.Merge(Api.Instance.GetContext()); // API context - evaluationContextBuilder.Merge(this.GetContext()); // Client context - evaluationContextBuilder.Merge(Api.Instance.GetTransactionContext()); // Transaction context - evaluationContextBuilder.Merge(context); // Invocation context - - var allHooks = ImmutableList.CreateBuilder() - .Concat(Api.Instance.GetHooks()) - .Concat(this.GetHooks()) - .Concat(options?.Hooks ?? Enumerable.Empty()) - .Concat(provider.GetProviderHooks()) - .ToImmutableList(); - - var sharedHookContext = new SharedHookContext( - flagKey, - defaultValue, - flagValueType, - this._metadata, - provider.GetMetadata() - ); - - FlagEvaluationDetails? evaluation = null; - var hookRunner = new HookRunner(allHooks, evaluationContextBuilder.Build(), sharedHookContext, - this._logger); - - try - { - var evaluationContextFromHooks = await hookRunner.TriggerBeforeHooksAsync(options?.HookHints, 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) - { - throw new ProviderNotReadyException("Provider has not yet completed initialization."); - } - else if (provider.Status == ProviderStatus.Fatal) - { - throw new ProviderFatalException("Provider is in an irrecoverable error state."); - } - - evaluation = - (await resolveValueDelegate - .Invoke(flagKey, defaultValue, evaluationContextFromHooks, cancellationToken) - .ConfigureAwait(false)) - .ToFlagEvaluationDetails(); - - if (evaluation.ErrorType == ErrorType.None) - { - await hookRunner.TriggerAfterHooksAsync( - evaluation, - options?.HookHints, - cancellationToken - ).ConfigureAwait(false); - } - else - { - var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage); - this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception); - await hookRunner.TriggerErrorHooksAsync(exception, options?.HookHints, cancellationToken) - .ConfigureAwait(false); - } + // short circuit evaluation entirely if provider is in a bad state + if (provider.Status == ProviderStatus.NotReady) + { + throw new ProviderNotReadyException("Provider has not yet completed initialization."); } - catch (FeatureProviderException ex) + else if (provider.Status == ProviderStatus.Fatal) { - this.FlagEvaluationErrorWithDescription(flagKey, ex.ErrorType.GetDescription(), ex); - evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error, - string.Empty, ex.Message); - await hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToken) - .ConfigureAwait(false); + throw new ProviderFatalException("Provider is in an irrecoverable error state."); } - catch (Exception ex) + + evaluation = + (await resolveValueDelegate + .Invoke(flagKey, defaultValue, evaluationContextFromHooks, cancellationToken) + .ConfigureAwait(false)) + .ToFlagEvaluationDetails(); + + if (evaluation.ErrorType == ErrorType.None) { - var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General; - evaluation = new FlagEvaluationDetails(flagKey, defaultValue, errorCode, Reason.Error, string.Empty, - ex.Message); - await hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToken) - .ConfigureAwait(false); + await hookRunner.TriggerAfterHooksAsync( + evaluation, + options?.HookHints, + cancellationToken + ).ConfigureAwait(false); } - finally + else { - evaluation ??= new FlagEvaluationDetails(flagKey, defaultValue, ErrorType.General, Reason.Error, - string.Empty, - "Evaluation failed to return a result."); - await hookRunner.TriggerFinallyHooksAsync(evaluation, options?.HookHints, cancellationToken) + var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage); + this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception); + await hookRunner.TriggerErrorHooksAsync(exception, options?.HookHints, cancellationToken) .ConfigureAwait(false); } - - return evaluation; } - - /// - /// Use this method to track user interactions and the application state. - /// - /// 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) - /// When trackingEventName is null or empty - public void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default) + catch (FeatureProviderException ex) { - if (string.IsNullOrWhiteSpace(trackingEventName)) - { - throw new ArgumentException("Tracking event cannot be null or empty.", nameof(trackingEventName)); - } - - var globalContext = Api.Instance.GetContext(); - var clientContext = this.GetContext(); + this.FlagEvaluationErrorWithDescription(flagKey, ex.ErrorType.GetDescription(), ex); + evaluation = new FlagEvaluationDetails(flagKey, defaultValue, ex.ErrorType, Reason.Error, + string.Empty, ex.Message); + 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 hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToken) + .ConfigureAwait(false); + } + finally + { + evaluation ??= new FlagEvaluationDetails(flagKey, defaultValue, ErrorType.General, Reason.Error, + string.Empty, + "Evaluation failed to return a result."); + await hookRunner.TriggerFinallyHooksAsync(evaluation, options?.HookHints, cancellationToken) + .ConfigureAwait(false); + } - var evaluationContextBuilder = EvaluationContext.Builder() - .Merge(globalContext) - .Merge(clientContext); - if (evaluationContext != null) evaluationContextBuilder.Merge(evaluationContext); + return evaluation; + } - this._providerAccessor.Invoke().Track(trackingEventName, evaluationContextBuilder.Build(), trackingEventDetails); + /// + /// Use this method to track user interactions and the application state. + /// + /// 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) + /// When trackingEventName is null or empty + public void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default) + { + if (string.IsNullOrWhiteSpace(trackingEventName)) + { + throw new ArgumentException("Tracking event cannot be null or empty.", nameof(trackingEventName)); } - [LoggerMessage(101, LogLevel.Error, "Error while evaluating flag {FlagKey}")] - partial void FlagEvaluationError(string flagKey, Exception exception); + var globalContext = Api.Instance.GetContext(); + var clientContext = this.GetContext(); - [LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")] - partial void HookReturnedNull(string hookName); + var evaluationContextBuilder = EvaluationContext.Builder() + .Merge(globalContext) + .Merge(clientContext); + if (evaluationContext != null) evaluationContextBuilder.Merge(evaluationContext); - [LoggerMessage(102, LogLevel.Error, "Error while evaluating flag {FlagKey}: {ErrorType}")] - partial void FlagEvaluationErrorWithDescription(string flagKey, string errorType, Exception exception); + 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); } diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 49f1de43..54e797db 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -9,284 +9,283 @@ using OpenFeature.Model; -namespace OpenFeature +namespace OpenFeature; + +/// +/// This class manages the collection of providers, both default and named, contained by the API. +/// +internal sealed partial class ProviderRepository : IAsyncDisposable { - /// - /// This class manages the collection of providers, both default and named, contained by the API. - /// - internal sealed partial class ProviderRepository : IAsyncDisposable - { - private ILogger _logger = NullLogger.Instance; + private ILogger _logger = NullLogger.Instance; - private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); + private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); - private readonly ConcurrentDictionary _featureProviders = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _featureProviders = + new ConcurrentDictionary(); - /// The reader/writer locks is not disposed because the singleton instance should never be disposed. - /// - /// Mutations of the _defaultProvider or _featureProviders are done within this lock even though - /// _featureProvider is a concurrent collection. This is for a couple of reasons, the first is that - /// a provider should only be shutdown if it is not in use, and it could be in use as either a named or - /// default provider. - /// - /// The second is that a concurrent collection doesn't provide any ordering, so we could check a provider - /// as it was being added or removed such as two concurrent calls to SetProvider replacing multiple instances - /// of that provider under different names. - private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); + /// The reader/writer locks is not disposed because the singleton instance should never be disposed. + /// + /// Mutations of the _defaultProvider or _featureProviders are done within this lock even though + /// _featureProvider is a concurrent collection. This is for a couple of reasons, the first is that + /// a provider should only be shutdown if it is not in use, and it could be in use as either a named or + /// default provider. + /// + /// The second is that a concurrent collection doesn't provide any ordering, so we could check a provider + /// as it was being added or removed such as two concurrent calls to SetProvider replacing multiple instances + /// of that provider under different names. + private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); - public async ValueTask DisposeAsync() + public async ValueTask DisposeAsync() + { + using (this._providersLock) { - using (this._providersLock) - { - await this.ShutdownAsync().ConfigureAwait(false); - } + await this.ShutdownAsync().ConfigureAwait(false); } + } - internal void SetLogger(ILogger logger) => this._logger = logger; + internal void SetLogger(ILogger logger) => this._logger = logger; - /// - /// Set the default provider - /// - /// the provider to set as the default, passing null has no effect - /// the context to initialize the provider with - /// - /// called after the provider has initialized successfully, only called if the provider needed initialization - /// - /// - /// called if an error happens during the initialization of the provider, only called if the provider needed - /// initialization - /// - public async Task SetProviderAsync( - FeatureProvider? featureProvider, - EvaluationContext context, - Func? afterInitSuccess = null, - Func? afterInitError = null) + /// + /// Set the default provider + /// + /// the provider to set as the default, passing null has no effect + /// the context to initialize the provider with + /// + /// called after the provider has initialized successfully, only called if the provider needed initialization + /// + /// + /// called if an error happens during the initialization of the provider, only called if the provider needed + /// initialization + /// + public async Task SetProviderAsync( + FeatureProvider? featureProvider, + EvaluationContext context, + Func? afterInitSuccess = null, + Func? afterInitError = null) + { + // Cannot unset the feature provider. + if (featureProvider == null) { - // Cannot unset the feature provider. - if (featureProvider == null) + return; + } + + this._providersLock.EnterWriteLock(); + // Default provider is swapped synchronously, initialization and shutdown may happen asynchronously. + try + { + // Setting the provider to the same provider should not have an effect. + if (ReferenceEquals(featureProvider, this._defaultProvider)) { return; } - this._providersLock.EnterWriteLock(); - // Default provider is swapped synchronously, initialization and shutdown may happen asynchronously. - try - { - // Setting the provider to the same provider should not have an effect. - if (ReferenceEquals(featureProvider, this._defaultProvider)) - { - return; - } + var oldProvider = this._defaultProvider; + this._defaultProvider = featureProvider; + // We want to allow shutdown to happen concurrently with initialization, and the caller to not + // wait for it. + _ = this.ShutdownIfUnusedAsync(oldProvider); + } + finally + { + this._providersLock.ExitWriteLock(); + } - var oldProvider = this._defaultProvider; - this._defaultProvider = featureProvider; - // We want to allow shutdown to happen concurrently with initialization, and the caller to not - // wait for it. - _ = this.ShutdownIfUnusedAsync(oldProvider); - } - finally - { - this._providersLock.ExitWriteLock(); - } + await InitProviderAsync(this._defaultProvider, context, afterInitSuccess, afterInitError) + .ConfigureAwait(false); + } - await InitProviderAsync(this._defaultProvider, context, afterInitSuccess, afterInitError) - .ConfigureAwait(false); + private static async Task InitProviderAsync( + FeatureProvider? newProvider, + EvaluationContext context, + Func? afterInitialization, + Func? afterError) + { + if (newProvider == null) + { + return; } - - private static async Task InitProviderAsync( - FeatureProvider? newProvider, - EvaluationContext context, - Func? afterInitialization, - Func? afterError) + if (newProvider.Status == ProviderStatus.NotReady) { - if (newProvider == null) - { - return; - } - if (newProvider.Status == ProviderStatus.NotReady) + try { - try + await newProvider.InitializeAsync(context).ConfigureAwait(false); + if (afterInitialization != null) { - await newProvider.InitializeAsync(context).ConfigureAwait(false); - if (afterInitialization != null) - { - await afterInitialization.Invoke(newProvider).ConfigureAwait(false); - } + await afterInitialization.Invoke(newProvider).ConfigureAwait(false); } - catch (Exception ex) + } + catch (Exception ex) + { + if (afterError != null) { - if (afterError != null) - { - await afterError.Invoke(newProvider, ex).ConfigureAwait(false); - } + await afterError.Invoke(newProvider, ex).ConfigureAwait(false); } } } + } - /// - /// Set a named provider - /// - /// an identifier which logically binds clients with providers - /// the provider to set as the default, passing null has no effect - /// the context to initialize the provider with - /// - /// called after the provider has initialized successfully, only called if the provider needed initialization - /// - /// - /// called if an error happens during the initialization of the provider, only called if the provider needed - /// initialization - /// - /// The to cancel any async side effects. - public async Task SetProviderAsync(string? domain, - FeatureProvider? featureProvider, - EvaluationContext context, - Func? afterInitSuccess = null, - Func? afterInitError = null, - CancellationToken cancellationToken = default) + /// + /// Set a named provider + /// + /// an identifier which logically binds clients with providers + /// the provider to set as the default, passing null has no effect + /// the context to initialize the provider with + /// + /// called after the provider has initialized successfully, only called if the provider needed initialization + /// + /// + /// called if an error happens during the initialization of the provider, only called if the provider needed + /// initialization + /// + /// The to cancel any async side effects. + public async Task SetProviderAsync(string? domain, + FeatureProvider? featureProvider, + EvaluationContext context, + Func? afterInitSuccess = null, + Func? afterInitError = null, + CancellationToken cancellationToken = default) + { + // Cannot set a provider for a null domain. + if (domain == null) { - // Cannot set a provider for a null domain. - if (domain == null) - { - return; - } + return; + } - this._providersLock.EnterWriteLock(); + this._providersLock.EnterWriteLock(); - try + try + { + this._featureProviders.TryGetValue(domain, out var oldProvider); + if (featureProvider != null) { - this._featureProviders.TryGetValue(domain, out var oldProvider); - if (featureProvider != null) - { - this._featureProviders.AddOrUpdate(domain, featureProvider, - (key, current) => featureProvider); - } - else - { - // If names of clients are programmatic, then setting the provider to null could result - // in unbounded growth of the collection. - this._featureProviders.TryRemove(domain, out _); - } - - // We want to allow shutdown to happen concurrently with initialization, and the caller to not - // wait for it. - _ = this.ShutdownIfUnusedAsync(oldProvider); + this._featureProviders.AddOrUpdate(domain, featureProvider, + (key, current) => featureProvider); } - finally + else { - this._providersLock.ExitWriteLock(); + // If names of clients are programmatic, then setting the provider to null could result + // in unbounded growth of the collection. + this._featureProviders.TryRemove(domain, out _); } - await InitProviderAsync(featureProvider, context, afterInitSuccess, afterInitError).ConfigureAwait(false); + // We want to allow shutdown to happen concurrently with initialization, and the caller to not + // wait for it. + _ = this.ShutdownIfUnusedAsync(oldProvider); } - - /// - /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. - /// - private async Task ShutdownIfUnusedAsync( - FeatureProvider? targetProvider) + finally { - if (ReferenceEquals(this._defaultProvider, targetProvider)) - { - return; - } + this._providersLock.ExitWriteLock(); + } - if (targetProvider != null && this._featureProviders.Values.Contains(targetProvider)) - { - return; - } + await InitProviderAsync(featureProvider, context, afterInitSuccess, afterInitError).ConfigureAwait(false); + } - await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); + /// + /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. + /// + private async Task ShutdownIfUnusedAsync( + FeatureProvider? targetProvider) + { + if (ReferenceEquals(this._defaultProvider, targetProvider)) + { + return; } - /// - /// - /// Shut down the provider and capture any exceptions thrown. - /// - /// - /// The provider is set either to a name or default before the old provider it shut down, so - /// it would not be meaningful to emit an error. - /// - /// - private async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider) + if (targetProvider != null && this._featureProviders.Values.Contains(targetProvider)) { - if (targetProvider == null) - { - return; - } + return; + } - try - { - await targetProvider.ShutdownAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - this.ErrorShuttingDownProvider(targetProvider.GetMetadata()?.Name, ex); - } + await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); + } + + /// + /// + /// Shut down the provider and capture any exceptions thrown. + /// + /// + /// The provider is set either to a name or default before the old provider it shut down, so + /// it would not be meaningful to emit an error. + /// + /// + private async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider) + { + if (targetProvider == null) + { + return; } - public FeatureProvider GetProvider() + try { - this._providersLock.EnterReadLock(); - try - { - return this._defaultProvider; - } - finally - { - this._providersLock.ExitReadLock(); - } + await targetProvider.ShutdownAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + this.ErrorShuttingDownProvider(targetProvider.GetMetadata()?.Name, ex); } + } - public FeatureProvider GetProvider(string? domain) + public FeatureProvider GetProvider() + { + this._providersLock.EnterReadLock(); + try + { + return this._defaultProvider; + } + finally { + this._providersLock.ExitReadLock(); + } + } + + public FeatureProvider GetProvider(string? domain) + { #if NET6_0_OR_GREATER - if (string.IsNullOrEmpty(domain)) - { - return this.GetProvider(); - } + if (string.IsNullOrEmpty(domain)) + { + return this.GetProvider(); + } #else - // This is a workaround for the issue in .NET Framework where string.IsNullOrEmpty is not nullable compatible. - if (domain == null || string.IsNullOrEmpty(domain)) - { - return this.GetProvider(); - } + // This is a workaround for the issue in .NET Framework where string.IsNullOrEmpty is not nullable compatible. + if (domain == null || string.IsNullOrEmpty(domain)) + { + return this.GetProvider(); + } #endif - return this._featureProviders.TryGetValue(domain, out var featureProvider) - ? featureProvider - : this.GetProvider(); - } + return this._featureProviders.TryGetValue(domain, out var featureProvider) + ? featureProvider + : this.GetProvider(); + } - public async Task ShutdownAsync(Action? afterError = null, CancellationToken cancellationToken = default) + public async Task ShutdownAsync(Action? afterError = null, CancellationToken cancellationToken = default) + { + var providers = new HashSet(); + this._providersLock.EnterWriteLock(); + try { - var providers = new HashSet(); - this._providersLock.EnterWriteLock(); - try + providers.Add(this._defaultProvider); + foreach (var featureProvidersValue in this._featureProviders.Values) { - providers.Add(this._defaultProvider); - foreach (var featureProvidersValue in this._featureProviders.Values) - { - providers.Add(featureProvidersValue); - } - - // Set a default provider so the Api is ready to be used again. - this._defaultProvider = new NoOpFeatureProvider(); - this._featureProviders.Clear(); - } - finally - { - this._providersLock.ExitWriteLock(); + providers.Add(featureProvidersValue); } - foreach (var targetProvider in providers) - { - // We don't need to take any actions after shutdown. - await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); - } + // Set a default provider so the Api is ready to be used again. + this._defaultProvider = new NoOpFeatureProvider(); + this._featureProviders.Clear(); + } + finally + { + this._providersLock.ExitWriteLock(); } - [LoggerMessage(EventId = 105, Level = LogLevel.Error, Message = "Error shutting down provider: {TargetProviderName}`")] - partial void ErrorShuttingDownProvider(string? targetProviderName, Exception exception); + foreach (var targetProvider in providers) + { + // We don't need to take any actions after shutdown. + await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); + } } + + [LoggerMessage(EventId = 105, Level = LogLevel.Error, Message = "Error shutting down provider: {TargetProviderName}`")] + partial void ErrorShuttingDownProvider(string? targetProviderName, Exception exception); } diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 7e125a89..fd8cf19f 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -4,75 +4,74 @@ using OpenFeature.Error; using OpenFeature.Model; -namespace OpenFeature.Providers.Memory +namespace OpenFeature.Providers.Memory; + +/// +/// Flag representation for the in-memory provider. +/// +public interface Flag; + +/// +/// Flag representation for the in-memory provider. +/// +public sealed class Flag : Flag { - /// - /// Flag representation for the in-memory provider. - /// - public interface Flag; + private readonly Dictionary _variants; + private readonly string _defaultVariant; + private readonly Func? _contextEvaluator; + private readonly ImmutableMetadata? _flagMetadata; /// /// Flag representation for the in-memory provider. /// - public sealed class Flag : Flag + /// dictionary of variants and their corresponding values + /// default variant (should match 1 key in variants dictionary) + /// optional context-sensitive evaluation function + /// optional metadata for the flag + public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null, ImmutableMetadata? flagMetadata = null) { - private readonly Dictionary _variants; - private readonly string _defaultVariant; - private readonly Func? _contextEvaluator; - private readonly ImmutableMetadata? _flagMetadata; + this._variants = variants; + this._defaultVariant = defaultVariant; + this._contextEvaluator = contextEvaluator; + this._flagMetadata = flagMetadata; + } - /// - /// Flag representation for the in-memory provider. - /// - /// dictionary of variants and their corresponding values - /// default variant (should match 1 key in variants dictionary) - /// optional context-sensitive evaluation function - /// optional metadata for the flag - public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null, ImmutableMetadata? flagMetadata = null) + internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) + { + T? value; + if (this._contextEvaluator == null) { - this._variants = variants; - this._defaultVariant = defaultVariant; - this._contextEvaluator = contextEvaluator; - this._flagMetadata = flagMetadata; + if (this._variants.TryGetValue(this._defaultVariant, out value)) + { + return new ResolutionDetails( + flagKey, + value, + variant: this._defaultVariant, + reason: Reason.Static, + flagMetadata: this._flagMetadata + ); + } + else + { + throw new GeneralException($"variant {this._defaultVariant} not found"); + } } - - internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) + else { - T? value; - if (this._contextEvaluator == null) + var variant = this._contextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); + if (!this._variants.TryGetValue(variant, out value)) { - if (this._variants.TryGetValue(this._defaultVariant, out value)) - { - return new ResolutionDetails( - flagKey, - value, - variant: this._defaultVariant, - reason: Reason.Static, - flagMetadata: this._flagMetadata - ); - } - else - { - throw new GeneralException($"variant {this._defaultVariant} not found"); - } + throw new GeneralException($"variant {variant} not found"); } else { - var variant = this._contextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); - if (!this._variants.TryGetValue(variant, out value)) - { - throw new GeneralException($"variant {variant} not found"); - } - else - { - return new ResolutionDetails( - flagKey, - value, - variant: variant, - reason: Reason.TargetingMatch, - flagMetadata: this._flagMetadata - ); - } + return new ResolutionDetails( + flagKey, + value, + variant: variant, + reason: Reason.TargetingMatch, + flagMetadata: this._flagMetadata + ); } } } diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 2eec879d..fce7afe1 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -5,113 +5,112 @@ using OpenFeature.Constant; using OpenFeature.Model; -namespace OpenFeature.Providers.Memory +namespace OpenFeature.Providers.Memory; + +/// +/// The in memory provider. +/// Useful for testing and demonstration purposes. +/// +/// In Memory Provider specification +public class InMemoryProvider : FeatureProvider { + private readonly Metadata _metadata = new Metadata("InMemory"); + + private Dictionary _flags; + + /// + public override Metadata GetMetadata() + { + return this._metadata; + } + /// - /// The in memory provider. - /// Useful for testing and demonstration purposes. + /// Construct a new InMemoryProvider. /// - /// In Memory Provider specification - public class InMemoryProvider : FeatureProvider + /// dictionary of Flags + public InMemoryProvider(IDictionary? flags = null) { - private readonly Metadata _metadata = new Metadata("InMemory"); - - private Dictionary _flags; - - /// - public override Metadata GetMetadata() + if (flags == null) { - return this._metadata; + this._flags = new Dictionary(); } - - /// - /// Construct a new InMemoryProvider. - /// - /// dictionary of Flags - public InMemoryProvider(IDictionary? flags = null) + else { - if (flags == null) - { - this._flags = new Dictionary(); - } - else - { - this._flags = new Dictionary(flags); // shallow copy - } + this._flags = new Dictionary(flags); // shallow copy } + } - /// - /// Update provider flag configuration, replacing all flags. - /// - /// the flags to use instead of the previous flags. - public async Task UpdateFlagsAsync(IDictionary? flags = null) + /// + /// Update provider flag configuration, replacing all flags. + /// + /// the flags to use instead of the previous flags. + public async Task UpdateFlagsAsync(IDictionary? flags = null) + { + var changed = this._flags.Keys.ToList(); + if (flags == null) { - var changed = this._flags.Keys.ToList(); - if (flags == null) - { - this._flags = new Dictionary(); - } - else - { - this._flags = new Dictionary(flags); // shallow copy - } - changed.AddRange(this._flags.Keys.ToList()); - var @event = new ProviderEventPayload - { - Type = ProviderEventTypes.ProviderConfigurationChanged, - ProviderName = this._metadata.Name, - FlagsChanged = changed, // emit all - Message = "flags changed", - }; - - await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false); + this._flags = new Dictionary(); } - - /// - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + else { - return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); + this._flags = new Dictionary(flags); // shallow copy } - - /// - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + changed.AddRange(this._flags.Keys.ToList()); + var @event = new ProviderEventPayload { - return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); - } + Type = ProviderEventTypes.ProviderConfigurationChanged, + ProviderName = this._metadata.Name, + FlagsChanged = changed, // emit all + Message = "flags changed", + }; - /// - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); - } + await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false); + } - /// - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); - } + /// + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); + } - /// - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + /// + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); + } + + private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext? context) + { + if (!this._flags.TryGetValue(flagKey, out var flag)) { - return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); + return new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, Reason.Error); } - private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext? context) + // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. + // In a production provider, such behavior is probably not desirable; consider supporting conversion. + if (flag is Flag value) { - if (!this._flags.TryGetValue(flagKey, out var flag)) - { - return new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, Reason.Error); - } - - // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. - // In a production provider, such behavior is probably not desirable; consider supporting conversion. - if (flag is Flag value) - { - return value.Evaluate(flagKey, defaultValue, context); - } - - return new ResolutionDetails(flagKey, defaultValue, ErrorType.TypeMismatch, Reason.Error); + return value.Evaluate(flagKey, defaultValue, context); } + + return new ResolutionDetails(flagKey, defaultValue, ErrorType.TypeMismatch, Reason.Error); } } diff --git a/src/OpenFeature/SharedHookContext.cs b/src/OpenFeature/SharedHookContext.cs index 3d6b787c..c364e40c 100644 --- a/src/OpenFeature/SharedHookContext.cs +++ b/src/OpenFeature/SharedHookContext.cs @@ -2,59 +2,58 @@ using OpenFeature.Constant; using OpenFeature.Model; -namespace OpenFeature +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) { /// - /// Component of the hook context which shared between all hook instances + /// Feature flag being evaluated /// - /// 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)); + public string FlagKey { get; } = flagKey ?? throw new ArgumentNullException(nameof(flagKey)); - /// - /// Default value if flag fails to be evaluated - /// - public T DefaultValue { get; } = defaultValue; + /// + /// Default value if flag fails to be evaluated + /// + public T DefaultValue { get; } = defaultValue; - /// - /// The value type of the flag - /// - public FlagValueType FlagValueType { get; } = flagValueType; + /// + /// The value type of the flag + /// + public FlagValueType FlagValueType { get; } = flagValueType; - /// - /// Client metadata - /// - public ClientMetadata ClientMetadata { get; } = - clientMetadata ?? throw new ArgumentNullException(nameof(clientMetadata)); + /// + /// Client metadata + /// + public ClientMetadata ClientMetadata { get; } = + clientMetadata ?? throw new ArgumentNullException(nameof(clientMetadata)); - /// - /// Provider metadata - /// - public Metadata ProviderMetadata { get; } = - providerMetadata ?? throw new ArgumentNullException(nameof(providerMetadata)); + /// + /// 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()); - } + /// + /// 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.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 03650144..c2779a31 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -7,100 +7,99 @@ using BenchmarkDotNet.Jobs; using OpenFeature.Model; -namespace OpenFeature.Benchmark +namespace OpenFeature.Benchmark; + +[MemoryDiagnoser] +[SimpleJob(RuntimeMoniker.Net60, baseline: true)] +[JsonExporterAttribute.Full] +[JsonExporterAttribute.FullCompressed] +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class OpenFeatureClientBenchmarks { - [MemoryDiagnoser] - [SimpleJob(RuntimeMoniker.Net60, baseline: true)] - [JsonExporterAttribute.Full] - [JsonExporterAttribute.FullCompressed] - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class OpenFeatureClientBenchmarks + private readonly string _domain; + private readonly string _clientVersion; + private readonly string _flagName; + private readonly bool _defaultBoolValue; + private readonly string _defaultStringValue; + private readonly int _defaultIntegerValue; + private readonly double _defaultDoubleValue; + private readonly Value _defaultStructureValue; + private readonly FlagEvaluationOptions _emptyFlagOptions; + private readonly FeatureClient _client; + + public OpenFeatureClientBenchmarks() { - private readonly string _domain; - private readonly string _clientVersion; - private readonly string _flagName; - private readonly bool _defaultBoolValue; - private readonly string _defaultStringValue; - private readonly int _defaultIntegerValue; - private readonly double _defaultDoubleValue; - private readonly Value _defaultStructureValue; - private readonly FlagEvaluationOptions _emptyFlagOptions; - private readonly FeatureClient _client; - - public OpenFeatureClientBenchmarks() - { - var fixture = new Fixture(); - this._domain = fixture.Create(); - this._clientVersion = fixture.Create(); - this._flagName = fixture.Create(); - this._defaultBoolValue = fixture.Create(); - this._defaultStringValue = fixture.Create(); - this._defaultIntegerValue = fixture.Create(); - this._defaultDoubleValue = fixture.Create(); - this._defaultStructureValue = fixture.Create(); - this._emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - - this._client = Api.Instance.GetClient(this._domain, this._clientVersion); - } - - [Benchmark] - public async Task OpenFeatureClient_GetBooleanValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue); - - [Benchmark] - public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty); - - [Benchmark] - public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty, this._emptyFlagOptions); - - [Benchmark] - public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue); - - [Benchmark] - public async Task OpenFeatureClient_GetStringValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty); - - [Benchmark] - public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithEmptyFlagEvaluationOptions() => - await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty, this._emptyFlagOptions); - - [Benchmark] - public async Task OpenFeatureClient_GetIntegerValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue); - - [Benchmark] - public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty); - - [Benchmark] - public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty, this._emptyFlagOptions); - - [Benchmark] - public async Task OpenFeatureClient_GetDoubleValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue); - - [Benchmark] - public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty); - - [Benchmark] - public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty, this._emptyFlagOptions); - - [Benchmark] - public async Task OpenFeatureClient_GetObjectValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue); - - [Benchmark] - public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty); - - [Benchmark] - public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty, this._emptyFlagOptions); + var fixture = new Fixture(); + this._domain = fixture.Create(); + this._clientVersion = fixture.Create(); + this._flagName = fixture.Create(); + this._defaultBoolValue = fixture.Create(); + this._defaultStringValue = fixture.Create(); + this._defaultIntegerValue = fixture.Create(); + this._defaultDoubleValue = fixture.Create(); + this._defaultStructureValue = fixture.Create(); + this._emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); + + this._client = Api.Instance.GetClient(this._domain, this._clientVersion); } + + [Benchmark] + public async Task OpenFeatureClient_GetBooleanValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue); + + [Benchmark] + public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty, this._emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue); + + [Benchmark] + public async Task OpenFeatureClient_GetStringValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithEmptyFlagEvaluationOptions() => + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty, this._emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetIntegerValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue); + + [Benchmark] + public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty, this._emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetDoubleValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue); + + [Benchmark] + public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty, this._emptyFlagOptions); + + [Benchmark] + public async Task OpenFeatureClient_GetObjectValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue); + + [Benchmark] + public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty); + + [Benchmark] + public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty, this._emptyFlagOptions); } diff --git a/test/OpenFeature.Benchmarks/Program.cs b/test/OpenFeature.Benchmarks/Program.cs index 0738b272..00be344a 100644 --- a/test/OpenFeature.Benchmarks/Program.cs +++ b/test/OpenFeature.Benchmarks/Program.cs @@ -1,12 +1,11 @@ using BenchmarkDotNet.Running; -namespace OpenFeature.Benchmark +namespace OpenFeature.Benchmark; + +internal class Program { - internal class Program + static void Main(string[] args) { - static void Main(string[] args) - { - BenchmarkRunner.Run(); - } + BenchmarkRunner.Run(); } } diff --git a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs index 1498056f..334f664e 100644 --- a/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderExceptionTests.cs @@ -4,65 +4,64 @@ using OpenFeature.Extension; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +public class FeatureProviderExceptionTests { - public class FeatureProviderExceptionTests + [Theory] + [InlineData(ErrorType.General, "GENERAL")] + [InlineData(ErrorType.ParseError, "PARSE_ERROR")] + [InlineData(ErrorType.TypeMismatch, "TYPE_MISMATCH")] + [InlineData(ErrorType.FlagNotFound, "FLAG_NOT_FOUND")] + [InlineData(ErrorType.ProviderNotReady, "PROVIDER_NOT_READY")] + public void FeatureProviderException_Should_Resolve_Description(ErrorType errorType, string errorDescription) { - [Theory] - [InlineData(ErrorType.General, "GENERAL")] - [InlineData(ErrorType.ParseError, "PARSE_ERROR")] - [InlineData(ErrorType.TypeMismatch, "TYPE_MISMATCH")] - [InlineData(ErrorType.FlagNotFound, "FLAG_NOT_FOUND")] - [InlineData(ErrorType.ProviderNotReady, "PROVIDER_NOT_READY")] - public void FeatureProviderException_Should_Resolve_Description(ErrorType errorType, string errorDescription) - { - var ex = new FeatureProviderException(errorType); + var ex = new FeatureProviderException(errorType); - Assert.Equal(errorDescription, ex.ErrorType.GetDescription()); - } + Assert.Equal(errorDescription, ex.ErrorType.GetDescription()); + } - [Theory] - [InlineData(ErrorType.General, "Subscription has expired, please renew your subscription.")] - [InlineData(ErrorType.ProviderNotReady, "User has exceeded the quota for this feature.")] - public void FeatureProviderException_Should_Allow_Custom_ErrorCode_Messages(ErrorType errorCode, string message) - { - var ex = new FeatureProviderException(errorCode, message, new ArgumentOutOfRangeException("flag")); + [Theory] + [InlineData(ErrorType.General, "Subscription has expired, please renew your subscription.")] + [InlineData(ErrorType.ProviderNotReady, "User has exceeded the quota for this feature.")] + public void FeatureProviderException_Should_Allow_Custom_ErrorCode_Messages(ErrorType errorCode, string message) + { + var ex = new FeatureProviderException(errorCode, message, new ArgumentOutOfRangeException("flag")); - Assert.Equal(errorCode, ex.ErrorType); - Assert.Equal(message, ex.Message); - Assert.IsType(ex.InnerException); - } + Assert.Equal(errorCode, ex.ErrorType); + Assert.Equal(message, ex.Message); + Assert.IsType(ex.InnerException); + } - private enum TestEnum - { - TestValueWithoutDescription - } + private enum TestEnum + { + TestValueWithoutDescription + } - [Fact] - public void GetDescription_WhenCalledWithEnumWithoutDescription_ReturnsEnumName() - { - // Arrange - var testEnum = TestEnum.TestValueWithoutDescription; - var expectedDescription = "TestValueWithoutDescription"; + [Fact] + public void GetDescription_WhenCalledWithEnumWithoutDescription_ReturnsEnumName() + { + // Arrange + var testEnum = TestEnum.TestValueWithoutDescription; + var expectedDescription = "TestValueWithoutDescription"; - // Act - var actualDescription = testEnum.GetDescription(); + // Act + var actualDescription = testEnum.GetDescription(); - // Assert - Assert.Equal(expectedDescription, actualDescription); - } + // Assert + Assert.Equal(expectedDescription, actualDescription); + } - [Fact] - public void GetDescription_WhenFieldIsNull_ReturnsEnumValueAsString() - { - // Arrange - var testEnum = (TestEnum)999;// This value should not exist in the TestEnum + [Fact] + public void GetDescription_WhenFieldIsNull_ReturnsEnumValueAsString() + { + // Arrange + var testEnum = (TestEnum)999;// This value should not exist in the TestEnum - // Act - var description = testEnum.GetDescription(); + // Act + var description = testEnum.GetDescription(); - // Assert - Assert.Equal(testEnum.ToString(), description); - } + // Assert + Assert.Equal(testEnum.ToString(), description); } } diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index ce1de36b..79ab2f00 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -7,132 +7,131 @@ using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class FeatureProviderTests : ClearOpenFeatureInstanceFixture + [Fact] + [Specification("2.1.1", "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")] + public void Provider_Must_Have_Metadata() + { + var provider = new TestProvider(); + + Assert.Equal(TestProvider.DefaultName, provider.GetMetadata().Name); + } + + [Fact] + [Specification("2.2.1", "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.")] + [Specification("2.2.2.1", "The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.")] + [Specification("2.2.3", "In cases of normal execution, the `provider` MUST populate the `resolution details` structure's `value` field with the resolved flag value.")] + [Specification("2.2.4", "In cases of normal execution, the `provider` SHOULD populate the `resolution details` structure's `variant` field with a string identifier corresponding to the returned flag value.")] + [Specification("2.2.5", "The `provider` SHOULD populate the `resolution details` structure's `reason` field with `\"STATIC\"`, `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"CACHED\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.")] + [Specification("2.2.6", "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error code` field, or otherwise must populate it with a null or falsy value.")] + [Specification("2.2.8.1", "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] + [Specification("2.3.2", "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error message` field, or otherwise must populate it with a null or falsy value.")] + public async Task Provider_Must_Resolve_Flag_Values() + { + var fixture = new Fixture(); + var flagName = fixture.Create(); + var defaultBoolValue = fixture.Create(); + var defaultStringValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); + var provider = new NoOpFeatureProvider(); + + var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(boolResolutionDetails, await provider.ResolveBooleanValueAsync(flagName, defaultBoolValue)); + + var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(integerResolutionDetails, await provider.ResolveIntegerValueAsync(flagName, defaultIntegerValue)); + + var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(doubleResolutionDetails, await provider.ResolveDoubleValueAsync(flagName, defaultDoubleValue)); + + var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(stringResolutionDetails, await provider.ResolveStringValueAsync(flagName, defaultStringValue)); + + var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, + ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(structureResolutionDetails, await provider.ResolveStructureValueAsync(flagName, defaultStructureValue)); + } + + [Fact] + [Specification("2.2.7", "In cases of abnormal execution, the `provider` MUST indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")] + [Specification("2.3.3", "In cases of abnormal execution, the `resolution details` structure's `error message` field MAY contain a string containing additional detail about the nature of the error.")] + public async Task Provider_Must_ErrorType() { - [Fact] - [Specification("2.1.1", "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")] - public void Provider_Must_Have_Metadata() - { - var provider = new TestProvider(); - - Assert.Equal(TestProvider.DefaultName, provider.GetMetadata().Name); - } - - [Fact] - [Specification("2.2.1", "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.")] - [Specification("2.2.2.1", "The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.")] - [Specification("2.2.3", "In cases of normal execution, the `provider` MUST populate the `resolution details` structure's `value` field with the resolved flag value.")] - [Specification("2.2.4", "In cases of normal execution, the `provider` SHOULD populate the `resolution details` structure's `variant` field with a string identifier corresponding to the returned flag value.")] - [Specification("2.2.5", "The `provider` SHOULD populate the `resolution details` structure's `reason` field with `\"STATIC\"`, `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"CACHED\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.")] - [Specification("2.2.6", "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error code` field, or otherwise must populate it with a null or falsy value.")] - [Specification("2.2.8.1", "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] - [Specification("2.3.2", "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error message` field, or otherwise must populate it with a null or falsy value.")] - public async Task Provider_Must_Resolve_Flag_Values() - { - var fixture = new Fixture(); - var flagName = fixture.Create(); - var defaultBoolValue = fixture.Create(); - var defaultStringValue = fixture.Create(); - var defaultIntegerValue = fixture.Create(); - var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); - var provider = new NoOpFeatureProvider(); - - var boolResolutionDetails = new ResolutionDetails(flagName, defaultBoolValue, ErrorType.None, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(boolResolutionDetails, await provider.ResolveBooleanValueAsync(flagName, defaultBoolValue)); - - var integerResolutionDetails = new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.None, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(integerResolutionDetails, await provider.ResolveIntegerValueAsync(flagName, defaultIntegerValue)); - - var doubleResolutionDetails = new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.None, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(doubleResolutionDetails, await provider.ResolveDoubleValueAsync(flagName, defaultDoubleValue)); - - var stringResolutionDetails = new ResolutionDetails(flagName, defaultStringValue, ErrorType.None, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(stringResolutionDetails, await provider.ResolveStringValueAsync(flagName, defaultStringValue)); - - var structureResolutionDetails = new ResolutionDetails(flagName, defaultStructureValue, - ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(structureResolutionDetails, await provider.ResolveStructureValueAsync(flagName, defaultStructureValue)); - } - - [Fact] - [Specification("2.2.7", "In cases of abnormal execution, the `provider` MUST indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")] - [Specification("2.3.3", "In cases of abnormal execution, the `resolution details` structure's `error message` field MAY contain a string containing additional detail about the nature of the error.")] - public async Task Provider_Must_ErrorType() - { - var fixture = new Fixture(); - var flagName = fixture.Create(); - var flagName2 = fixture.Create(); - var defaultBoolValue = fixture.Create(); - var defaultStringValue = fixture.Create(); - var defaultIntegerValue = fixture.Create(); - var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); - var providerMock = Substitute.For(); - const string testMessage = "An error message"; - - providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue, Arg.Any()) - .Returns(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - - providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue, Arg.Any()) - .Returns(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - - providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue, Arg.Any()) - .Returns(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.InvalidContext, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - - providerMock.ResolveStringValueAsync(flagName, defaultStringValue, Arg.Any()) - .Returns(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - - providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue, Arg.Any()) - .Returns(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - - providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue, Arg.Any()) - .Returns(new ResolutionDetails(flagName2, defaultStructureValue, ErrorType.ProviderNotReady, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); - - providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue, Arg.Any()) - .Returns(new ResolutionDetails(flagName2, defaultBoolValue, ErrorType.TargetingKeyMissing, - NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); - - var boolRes = await providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue); - Assert.Equal(ErrorType.General, boolRes.ErrorType); - Assert.Equal(testMessage, boolRes.ErrorMessage); - - var intRes = await providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue); - Assert.Equal(ErrorType.ParseError, intRes.ErrorType); - Assert.Equal(testMessage, intRes.ErrorMessage); - - var doubleRes = await providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue); - Assert.Equal(ErrorType.InvalidContext, doubleRes.ErrorType); - Assert.Equal(testMessage, doubleRes.ErrorMessage); - - var stringRes = await providerMock.ResolveStringValueAsync(flagName, defaultStringValue); - Assert.Equal(ErrorType.TypeMismatch, stringRes.ErrorType); - Assert.Equal(testMessage, stringRes.ErrorMessage); - - var structRes1 = await providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue); - Assert.Equal(ErrorType.FlagNotFound, structRes1.ErrorType); - Assert.Equal(testMessage, structRes1.ErrorMessage); - - var structRes2 = await providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue); - Assert.Equal(ErrorType.ProviderNotReady, structRes2.ErrorType); - Assert.Equal(testMessage, structRes2.ErrorMessage); - - var boolRes2 = await providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue); - Assert.Equal(ErrorType.TargetingKeyMissing, boolRes2.ErrorType); - Assert.Null(boolRes2.ErrorMessage); - } + var fixture = new Fixture(); + var flagName = fixture.Create(); + var flagName2 = fixture.Create(); + var defaultBoolValue = fixture.Create(); + var defaultStringValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); + var providerMock = Substitute.For(); + const string testMessage = "An error message"; + + providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultIntegerValue, ErrorType.ParseError, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultDoubleValue, ErrorType.InvalidContext, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.ResolveStringValueAsync(flagName, defaultStringValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultStringValue, ErrorType.TypeMismatch, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName, defaultStructureValue, ErrorType.FlagNotFound, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName2, defaultStructureValue, ErrorType.ProviderNotReady, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant, testMessage)); + + providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue, Arg.Any()) + .Returns(new ResolutionDetails(flagName2, defaultBoolValue, ErrorType.TargetingKeyMissing, + NoOpProvider.ReasonNoOp, NoOpProvider.Variant)); + + var boolRes = await providerMock.ResolveBooleanValueAsync(flagName, defaultBoolValue); + Assert.Equal(ErrorType.General, boolRes.ErrorType); + Assert.Equal(testMessage, boolRes.ErrorMessage); + + var intRes = await providerMock.ResolveIntegerValueAsync(flagName, defaultIntegerValue); + Assert.Equal(ErrorType.ParseError, intRes.ErrorType); + Assert.Equal(testMessage, intRes.ErrorMessage); + + var doubleRes = await providerMock.ResolveDoubleValueAsync(flagName, defaultDoubleValue); + Assert.Equal(ErrorType.InvalidContext, doubleRes.ErrorType); + Assert.Equal(testMessage, doubleRes.ErrorMessage); + + var stringRes = await providerMock.ResolveStringValueAsync(flagName, defaultStringValue); + Assert.Equal(ErrorType.TypeMismatch, stringRes.ErrorType); + Assert.Equal(testMessage, stringRes.ErrorMessage); + + var structRes1 = await providerMock.ResolveStructureValueAsync(flagName, defaultStructureValue); + Assert.Equal(ErrorType.FlagNotFound, structRes1.ErrorType); + Assert.Equal(testMessage, structRes1.ErrorMessage); + + var structRes2 = await providerMock.ResolveStructureValueAsync(flagName2, defaultStructureValue); + Assert.Equal(ErrorType.ProviderNotReady, structRes2.ErrorType); + Assert.Equal(testMessage, structRes2.ErrorMessage); + + var boolRes2 = await providerMock.ResolveBooleanValueAsync(flagName2, defaultBoolValue); + Assert.Equal(ErrorType.TargetingKeyMissing, boolRes2.ErrorType); + Assert.Null(boolRes2.ErrorMessage); } } diff --git a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs index 7f299504..461455eb 100644 --- a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs @@ -8,661 +8,660 @@ using OpenFeature.Model; using Xunit; -namespace OpenFeature.Tests.Hooks +namespace OpenFeature.Tests.Hooks; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] + +public class LoggingHookTests { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] + [Fact] + public async Task BeforeAsync_Without_EvaluationContext_Generates_Debug_Log() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + // Act + await hook.BeforeAsync(context); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + + Assert.Equal(LogLevel.Debug, record.Level); + Assert.Equal( + """ + Before Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + + """, + record.Message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task BeforeAsync_Without_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + // Act + await hook.BeforeAsync(context); + + // Assert + var record = logger.LatestRecord; + + Assert.Equal( + """ + Before Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + + """, + record.Message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task BeforeAsync_With_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var timestamp = DateTime.Parse("2025-01-01T11:00:00.0000000Z"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", "value") + .Set("key_2", false) + .Set("key_3", 1.531) + .Set("key_4", 42) + .Set("key_5", timestamp) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.BeforeAsync(context); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + + Assert.Multiple( + () => Assert.Contains("key_1:value", record.Message), + () => Assert.Contains("key_2:False", record.Message), + () => Assert.Contains("key_3:1.531", record.Message), + () => Assert.Contains("key_4:42", record.Message), + () => Assert.Contains($"key_5:{timestamp:O}", record.Message) + ); + } + + [Fact] + public async Task BeforeAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + // Act + var hook = new LoggingHook(logger, includeContext: true); + + await hook.BeforeAsync(context); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + + Assert.Equal( + """ + Before Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + + """, + record.Message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task ErrorAsync_Without_EvaluationContext_Generates_Error_Log() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + var exception = new Exception("Error within hook!"); + + // Act + await hook.ErrorAsync(context, exception); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + + Assert.Equal(LogLevel.Error, record.Level); + } + + [Fact] + public async Task ErrorAsync_Without_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var hook = new LoggingHook(logger, includeContext: false); + + var exception = new Exception("Error within hook!"); + + // Act + await hook.ErrorAsync(context, exception); + + // Assert + var record = logger.LatestRecord; + + Assert.Equal( + """ + Error during Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + + """, + record.Message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task ErrorAsync_With_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + + var timestamp = DateTime.Parse("2099-01-01T01:00:00.0000000Z"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", " ") + .Set("key_2", true) + .Set("key_3", 0.002154) + .Set("key_4", -15) + .Set("key_5", timestamp) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var hook = new LoggingHook(logger, includeContext: true); - public class LoggingHookTests + var exception = new Exception("Error within hook!"); + + // Act + await hook.ErrorAsync(context, exception); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Error, record.Level); + + Assert.Multiple( + () => Assert.Contains("key_1: ", record.Message), + () => Assert.Contains("key_2:True", record.Message), + () => Assert.Contains("key_3:0.002154", record.Message), + () => Assert.Contains("key_4:-15", record.Message), + () => Assert.Contains($"key_5:{timestamp:O}", record.Message) + ); + } + + [Fact] + public async Task ErrorAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() { - [Fact] - public async Task BeforeAsync_Without_EvaluationContext_Generates_Debug_Log() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - var hook = new LoggingHook(logger, includeContext: false); - - // Act - await hook.BeforeAsync(context); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - - Assert.Equal(LogLevel.Debug, record.Level); - Assert.Equal( - """ - Before Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - - """, - record.Message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task BeforeAsync_Without_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - var hook = new LoggingHook(logger, includeContext: false); - - // Act - await hook.BeforeAsync(context); - - // Assert - var record = logger.LatestRecord; - - Assert.Equal( - """ - Before Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - - """, - record.Message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task BeforeAsync_With_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var timestamp = DateTime.Parse("2025-01-01T11:00:00.0000000Z"); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", "value") - .Set("key_2", false) - .Set("key_3", 1.531) - .Set("key_4", 42) - .Set("key_5", timestamp) - .Build(); - - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); - - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.BeforeAsync(context); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - Assert.Equal(LogLevel.Debug, record.Level); - - Assert.Multiple( - () => Assert.Contains("key_1:value", record.Message), - () => Assert.Contains("key_2:False", record.Message), - () => Assert.Contains("key_3:1.531", record.Message), - () => Assert.Contains("key_4:42", record.Message), - () => Assert.Contains($"key_5:{timestamp:O}", record.Message) - ); - } - - [Fact] - public async Task BeforeAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - // Act - var hook = new LoggingHook(logger, includeContext: true); - - await hook.BeforeAsync(context); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - Assert.Equal(LogLevel.Debug, record.Level); - - Assert.Equal( - """ - Before Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - - """, - record.Message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task ErrorAsync_Without_EvaluationContext_Generates_Error_Log() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - var hook = new LoggingHook(logger, includeContext: false); - - var exception = new Exception("Error within hook!"); - - // Act - await hook.ErrorAsync(context, exception); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - - Assert.Equal(LogLevel.Error, record.Level); - } - - [Fact] - public async Task ErrorAsync_Without_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); + // Arrange + var logger = new FakeLogger(); - var hook = new LoggingHook(logger, includeContext: false); + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); - var exception = new Exception("Error within hook!"); + var hook = new LoggingHook(logger, includeContext: true); - // Act - await hook.ErrorAsync(context, exception); - - // Assert - var record = logger.LatestRecord; - - Assert.Equal( - """ - Error during Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - - """, - record.Message, - ignoreLineEndingDifferences: true - ); - } + var exception = new Exception("Error within hook!"); - [Fact] - public async Task ErrorAsync_With_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); + // Act + await hook.ErrorAsync(context, exception); - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); + // Assert + Assert.Equal(1, logger.Collector.Count); - var timestamp = DateTime.Parse("2099-01-01T01:00:00.0000000Z"); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", " ") - .Set("key_2", true) - .Set("key_3", 0.002154) - .Set("key_4", -15) - .Set("key_5", timestamp) - .Build(); + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Error, record.Level); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); + Assert.Equal( + """ + Error during Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: - var hook = new LoggingHook(logger, includeContext: true); + """, + record.Message, + ignoreLineEndingDifferences: true + ); + } - var exception = new Exception("Error within hook!"); + [Fact] + public async Task AfterAsync_Without_EvaluationContext_Generates_Debug_Log() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: false); + + // Act + await hook.AfterAsync(context, details); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + } - // Act - await hook.ErrorAsync(context, exception); + [Fact] + public async Task AfterAsync_Without_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); - // Assert - Assert.Equal(1, logger.Collector.Count); + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); - var record = logger.LatestRecord; - Assert.Equal(LogLevel.Error, record.Level); + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - Assert.Multiple( - () => Assert.Contains("key_1: ", record.Message), - () => Assert.Contains("key_2:True", record.Message), - () => Assert.Contains("key_3:0.002154", record.Message), - () => Assert.Contains("key_4:-15", record.Message), - () => Assert.Contains($"key_5:{timestamp:O}", record.Message) - ); - } + var hook = new LoggingHook(logger, includeContext: false); - [Fact] - public async Task ErrorAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); + // Act + await hook.AfterAsync(context, details); - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - var hook = new LoggingHook(logger, includeContext: true); - - var exception = new Exception("Error within hook!"); - - // Act - await hook.ErrorAsync(context, exception); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - Assert.Equal(LogLevel.Error, record.Level); - - Assert.Equal( - """ - Error during Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - - """, - record.Message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task AfterAsync_Without_EvaluationContext_Generates_Debug_Log() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - - var hook = new LoggingHook(logger, includeContext: false); - - // Act - await hook.AfterAsync(context, details); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - Assert.Equal(LogLevel.Debug, record.Level); - } + // Assert + var record = logger.LatestRecord; - [Fact] - public async Task AfterAsync_Without_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); + """, + record.Message, + ignoreLineEndingDifferences: true + ); + } - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + [Fact] + public async Task AfterAsync_With_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); - var hook = new LoggingHook(logger, includeContext: false); + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", "") + .Set("key_2", false) + .Set("key_3", double.MinValue) + .Set("key_4", int.MaxValue) + .Set("key_5", DateTime.MinValue) + .Build(); - // Act - await hook.AfterAsync(context, details); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); - // Assert - var record = logger.LatestRecord; - - Assert.Equal( - """ - After Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - - """, - record.Message, - ignoreLineEndingDifferences: true - ); - } + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - [Fact] - public async Task AfterAsync_With_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); + var hook = new LoggingHook(logger, includeContext: true); - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", "") - .Set("key_2", false) - .Set("key_3", double.MinValue) - .Set("key_4", int.MaxValue) - .Set("key_5", DateTime.MinValue) - .Build(); + // Act + await hook.AfterAsync(context, details); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); - - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + // Assert + Assert.Equal(1, logger.Collector.Count); - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.AfterAsync(context, details); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - Assert.Equal(LogLevel.Debug, record.Level); + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); - // .NET Framework uses G15 formatter on double.ToString - // .NET uses G17 formatter on double.ToString + // .NET Framework uses G15 formatter on double.ToString + // .NET uses G17 formatter on double.ToString #if NET462 - var expectedMaxDoubleString = "-1.79769313486232E+308"; + var expectedMaxDoubleString = "-1.79769313486232E+308"; #else - var expectedMaxDoubleString = "-1.7976931348623157E+308"; + var expectedMaxDoubleString = "-1.7976931348623157E+308"; #endif - Assert.Multiple( - () => Assert.Contains("key_1:", record.Message), - () => Assert.Contains("key_2:False", record.Message), - () => Assert.Contains($"key_3:{expectedMaxDoubleString}", record.Message), - () => Assert.Contains("key_4:2147483647", record.Message), - () => Assert.Contains("key_5:0001-01-01T00:00:00.0000000", record.Message) - ); - } - - [Fact] - public async Task AfterAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.AfterAsync(context, details); - - // Assert - Assert.Equal(1, logger.Collector.Count); - - var record = logger.LatestRecord; - Assert.Equal(LogLevel.Debug, record.Level); - - Assert.Equal( - """ - After Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - - """, - record.Message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public void Create_LoggingHook_Without_Logger() - { - Assert.Throws(() => new LoggingHook(null!, includeContext: true)); - } - - [Fact] - public async Task With_Structure_Type_In_Context_Returns_Qualified_Name_Of_Value() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", Structure.Builder().Set("inner_key_1", false).Build()) - .Build(); - - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); - - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.AfterAsync(context, details); - - // Assert - var record = logger.LatestRecord; - - // Raw string literals will convert tab to spaces (the File index style) - var message = NormalizeLogRecord(record); - - Assert.Equal( - """ - After Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - key_1:OpenFeature.Model.Value - - """, - message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task Without_Domain_Returns_Missing() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata(null, "1.0.0"); - var providerMetadata = new Metadata("provider"); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", true) - .Build(); - - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); - - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.AfterAsync(context, details); - - // Assert - var record = logger.LatestRecord; - var message = NormalizeLogRecord(record); - - Assert.Equal( - """ - After Flag Evaluation Domain:missing - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - key_1:True - - """, - message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task Without_Provider_Returns_Missing() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata(null); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", true) - .Build(); - - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); - - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.AfterAsync(context, details); - - // Assert - var record = logger.LatestRecord; - var message = NormalizeLogRecord(record); - - Assert.Equal( - """ - After Flag Evaluation Domain:client - ProviderName:missing - FlagKey:test - DefaultValue:False - Context: - key_1:True - - """, - message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task Without_DefaultValue_Returns_Missing() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", true) - .Build(); - - var context = new HookContext("test", null!, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); - - var details = new FlagEvaluationDetails("test", "true", ErrorType.None, reason: null, variant: null); - - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.AfterAsync(context, details); - - // Assert - var record = logger.LatestRecord; - var message = NormalizeLogRecord(record); - - Assert.Equal( - """ - After Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:missing - Context: - key_1:True - - """, - message, - ignoreLineEndingDifferences: true - ); - } - - [Fact] - public async Task Without_EvaluationContextValue_Returns_Nothing() - { - // Arrange - var logger = new FakeLogger(); - - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var evaluationContext = EvaluationContext.Builder() - .Set("key_1", (string)null!) - .Build(); - - var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, - providerMetadata, evaluationContext); - - var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); - - var hook = new LoggingHook(logger, includeContext: true); - - // Act - await hook.AfterAsync(context, details); - - // Assert - var record = logger.LatestRecord; - var message = NormalizeLogRecord(record); - - Assert.Equal( - """ - After Flag Evaluation Domain:client - ProviderName:provider - FlagKey:test - DefaultValue:False - Context: - key_1: - - """, - message, - ignoreLineEndingDifferences: true - ); - } - - private static string NormalizeLogRecord(FakeLogRecord record) - { - // Raw string literals will convert tab to spaces (the File index style) - const int tabSize = 4; - - return record.Message.Replace("\t", new string(' ', tabSize)); - } + Assert.Multiple( + () => Assert.Contains("key_1:", record.Message), + () => Assert.Contains("key_2:False", record.Message), + () => Assert.Contains($"key_3:{expectedMaxDoubleString}", record.Message), + () => Assert.Contains("key_4:2147483647", record.Message), + () => Assert.Contains("key_5:0001-01-01T00:00:00.0000000", record.Message) + ); + } + + [Fact] + public async Task AfterAsync_With_No_EvaluationContext_Generates_Correct_Log_Message() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + Assert.Equal(1, logger.Collector.Count); + + var record = logger.LatestRecord; + Assert.Equal(LogLevel.Debug, record.Level); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + + """, + record.Message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public void Create_LoggingHook_Without_Logger() + { + Assert.Throws(() => new LoggingHook(null!, includeContext: true)); + } + + [Fact] + public async Task With_Structure_Type_In_Context_Returns_Qualified_Name_Of_Value() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", Structure.Builder().Set("inner_key_1", false).Build()) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + + // Raw string literals will convert tab to spaces (the File index style) + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + key_1:OpenFeature.Model.Value + + """, + message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task Without_Domain_Returns_Missing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata(null, "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", true) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:missing + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + key_1:True + + """, + message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task Without_Provider_Returns_Missing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata(null); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", true) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:missing + FlagKey:test + DefaultValue:False + Context: + key_1:True + + """, + message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task Without_DefaultValue_Returns_Missing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", true) + .Build(); + + var context = new HookContext("test", null!, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", "true", ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:missing + Context: + key_1:True + + """, + message, + ignoreLineEndingDifferences: true + ); + } + + [Fact] + public async Task Without_EvaluationContextValue_Returns_Nothing() + { + // Arrange + var logger = new FakeLogger(); + + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var evaluationContext = EvaluationContext.Builder() + .Set("key_1", (string)null!) + .Build(); + + var context = new HookContext("test", false, FlagValueType.Object, clientMetadata, + providerMetadata, evaluationContext); + + var details = new FlagEvaluationDetails("test", true, ErrorType.None, reason: null, variant: null); + + var hook = new LoggingHook(logger, includeContext: true); + + // Act + await hook.AfterAsync(context, details); + + // Assert + var record = logger.LatestRecord; + var message = NormalizeLogRecord(record); + + Assert.Equal( + """ + After Flag Evaluation Domain:client + ProviderName:provider + FlagKey:test + DefaultValue:False + Context: + key_1: + + """, + message, + ignoreLineEndingDifferences: true + ); + } + + private static string NormalizeLogRecord(FakeLogRecord record) + { + // Raw string literals will convert tab to spaces (the File index style) + const int tabSize = 4; + + return record.Message.Replace("\t", new string(' ', tabSize)); } } diff --git a/test/OpenFeature.Tests/Internal/SpecificationAttribute.cs b/test/OpenFeature.Tests/Internal/SpecificationAttribute.cs index 7c0aac2a..9fea9fef 100644 --- a/test/OpenFeature.Tests/Internal/SpecificationAttribute.cs +++ b/test/OpenFeature.Tests/Internal/SpecificationAttribute.cs @@ -1,17 +1,16 @@ using System; -namespace OpenFeature.Tests.Internal +namespace OpenFeature.Tests.Internal; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] +public class SpecificationAttribute : Attribute { - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] - public class SpecificationAttribute : Attribute - { - public string Code { get; } - public string Description { get; } + public string Code { get; } + public string Description { get; } - public SpecificationAttribute(string code, string description) - { - this.Code = code; - this.Description = description; - } + public SpecificationAttribute(string code, string description) + { + this.Code = code; + this.Description = description; } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index fc6f415f..31450a6f 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -16,665 +16,664 @@ using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture + [Fact] + [Specification("1.2.1", "The client MUST provide a method to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] + public void OpenFeatureClient_Should_Allow_Hooks() { - [Fact] - [Specification("1.2.1", "The client MUST provide a method to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] - public void OpenFeatureClient_Should_Allow_Hooks() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var hook1 = Substitute.For(); - var hook2 = Substitute.For(); - var hook3 = Substitute.For(); + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + var hook3 = Substitute.For(); - var client = Api.Instance.GetClient(domain, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); - client.AddHooks(new[] { hook1, hook2 }); + client.AddHooks(new[] { hook1, hook2 }); - var expectedHooks = new[] { hook1, hook2 }.AsEnumerable(); - Assert.Equal(expectedHooks, client.GetHooks()); + var expectedHooks = new[] { hook1, hook2 }.AsEnumerable(); + Assert.Equal(expectedHooks, client.GetHooks()); - client.AddHooks(hook3); + client.AddHooks(hook3); - expectedHooks = new[] { hook1, hook2, hook3 }.AsEnumerable(); - Assert.Equal(expectedHooks, client.GetHooks()); + expectedHooks = new[] { hook1, hook2, hook3 }.AsEnumerable(); + Assert.Equal(expectedHooks, client.GetHooks()); - client.ClearHooks(); - Assert.Empty(client.GetHooks()); - } + client.ClearHooks(); + Assert.Empty(client.GetHooks()); + } - [Fact] - [Specification("1.2.2", "The client interface MUST define a `metadata` member or accessor, containing an immutable `name` field or accessor of type string, which corresponds to the `name` value supplied during client creation.")] - public void OpenFeatureClient_Metadata_Should_Have_Name() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var client = Api.Instance.GetClient(domain, clientVersion); + [Fact] + [Specification("1.2.2", "The client interface MUST define a `metadata` member or accessor, containing an immutable `name` field or accessor of type string, which corresponds to the `name` value supplied during client creation.")] + public void OpenFeatureClient_Metadata_Should_Have_Name() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var client = Api.Instance.GetClient(domain, clientVersion); - Assert.Equal(domain, client.GetMetadata().Name); - Assert.Equal(clientVersion, client.GetMetadata().Version); - } + Assert.Equal(domain, client.GetMetadata().Name); + Assert.Equal(clientVersion, client.GetMetadata().Version); + } - [Fact] - [Specification("1.3.1", "The `client` MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns the flag value.")] - [Specification("1.3.2.1", "The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")] - [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] - public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultBoolValue = fixture.Create(); - var defaultStringValue = fixture.Create(); - var defaultIntegerValue = fixture.Create(); - var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); - var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - - await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); - var client = Api.Instance.GetClient(domain, clientVersion); - - Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue)); - Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)); - Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)); - - Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue)); - Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)); - Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)); - - Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue)); - Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)); - Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)); - - Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue)); - Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty)); - Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)); - - Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue)); - Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty)); - Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)); - } + [Fact] + [Specification("1.3.1", "The `client` MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns the flag value.")] + [Specification("1.3.2.1", "The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")] + [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] + public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultBoolValue = fixture.Create(); + var defaultStringValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); + var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); + + await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); + var client = Api.Instance.GetClient(domain, clientVersion); + + Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue)); + Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)); + Assert.Equal(defaultBoolValue, await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)); + + Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue)); + Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)); + Assert.Equal(defaultIntegerValue, await client.GetIntegerValueAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)); + + Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue)); + Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)); + Assert.Equal(defaultDoubleValue, await client.GetDoubleValueAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)); + + Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue)); + Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty)); + Assert.Equal(defaultStringValue, await client.GetStringValueAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)); + + Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue)); + Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty)); + Assert.Equivalent(defaultStructureValue, await client.GetObjectValueAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)); + } - [Fact] - [Specification("1.4.1", "The `client` MUST provide methods for detailed flag value evaluation with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns an `evaluation details` structure.")] - [Specification("1.4.2", "The `evaluation details` structure's `value` field MUST contain the evaluated flag value.")] - [Specification("1.4.3.1", "The `evaluation details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] - [Specification("1.4.4", "The `evaluation details` structure's `flag key` field MUST contain the `flag key` argument passed to the detailed flag evaluation method.")] - [Specification("1.4.5", "In cases of normal execution, the `evaluation details` structure's `variant` field MUST contain the value of the `variant` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")] - [Specification("1.4.6", "In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")] - [Specification("1.4.11", "The `client` SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")] - [Specification("2.2.8.1", "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] - public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultBoolValue = fixture.Create(); - var defaultStringValue = fixture.Create(); - var defaultIntegerValue = fixture.Create(); - var defaultDoubleValue = fixture.Create(); - var defaultStructureValue = fixture.Create(); - var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - - await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); - var client = Api.Instance.GetClient(domain, clientVersion); - - var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)); - Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty)); - Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)); - - var integerFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue)); - Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)); - Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)); - - var doubleFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue)); - Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)); - Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)); - - var stringFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue)); - Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty)); - Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)); - - var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); - Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue)); - Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty)); - Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)); - } + [Fact] + [Specification("1.4.1", "The `client` MUST provide methods for detailed flag value evaluation with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns an `evaluation details` structure.")] + [Specification("1.4.2", "The `evaluation details` structure's `value` field MUST contain the evaluated flag value.")] + [Specification("1.4.3.1", "The `evaluation details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] + [Specification("1.4.4", "The `evaluation details` structure's `flag key` field MUST contain the `flag key` argument passed to the detailed flag evaluation method.")] + [Specification("1.4.5", "In cases of normal execution, the `evaluation details` structure's `variant` field MUST contain the value of the `variant` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")] + [Specification("1.4.6", "In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")] + [Specification("1.4.11", "The `client` SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")] + [Specification("2.2.8.1", "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")] + public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultBoolValue = fixture.Create(); + var defaultStringValue = fixture.Create(); + var defaultIntegerValue = fixture.Create(); + var defaultDoubleValue = fixture.Create(); + var defaultStructureValue = fixture.Create(); + var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); + + await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); + var client = Api.Instance.GetClient(domain, clientVersion); + + var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)); + Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty)); + Assert.Equivalent(boolFlagEvaluationDetails, await client.GetBooleanDetailsAsync(flagName, defaultBoolValue, EvaluationContext.Empty, emptyFlagOptions)); + + var integerFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultIntegerValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue)); + Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty)); + Assert.Equivalent(integerFlagEvaluationDetails, await client.GetIntegerDetailsAsync(flagName, defaultIntegerValue, EvaluationContext.Empty, emptyFlagOptions)); + + var doubleFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultDoubleValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue)); + Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty)); + Assert.Equivalent(doubleFlagEvaluationDetails, await client.GetDoubleDetailsAsync(flagName, defaultDoubleValue, EvaluationContext.Empty, emptyFlagOptions)); + + var stringFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStringValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue)); + Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty)); + Assert.Equivalent(stringFlagEvaluationDetails, await client.GetStringDetailsAsync(flagName, defaultStringValue, EvaluationContext.Empty, emptyFlagOptions)); + + var structureFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultStructureValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); + Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue)); + Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty)); + Assert.Equivalent(structureFlagEvaluationDetails, await client.GetObjectDetailsAsync(flagName, defaultStructureValue, EvaluationContext.Empty, emptyFlagOptions)); + } - [Fact] - [Specification("1.1.2", "The `API` MUST provide a function to set the default `provider`, which accepts an API-conformant `provider` implementation.")] - [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] - [Specification("1.4.7", "In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST contain an `error code`.")] - [Specification("1.4.8", "In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")] - [Specification("1.4.9", "Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")] - [Specification("1.4.10", "In the case of abnormal execution, the client SHOULD log an informative error message.")] - public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatch() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); - var mockedFeatureProvider = Substitute.For(); - var mockedLogger = Substitute.For>(); + [Fact] + [Specification("1.1.2", "The `API` MUST provide a function to set the default `provider`, which accepts an API-conformant `provider` implementation.")] + [Specification("1.3.3", "The `client` SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.")] + [Specification("1.4.7", "In cases of abnormal execution, the `evaluation details` structure's `error code` field MUST contain an `error code`.")] + [Specification("1.4.8", "In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")] + [Specification("1.4.9", "Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")] + [Specification("1.4.10", "In the case of abnormal execution, the client SHOULD log an informative error message.")] + public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatch() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + var mockedFeatureProvider = Substitute.For(); + var mockedLogger = Substitute.For>(); - // This will fail to case a String to TestStructure - mockedFeatureProvider.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(); - mockedFeatureProvider.GetMetadata().Returns(new Metadata(fixture.Create())); - mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + // This will fail to case a String to TestStructure + mockedFeatureProvider.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(); + mockedFeatureProvider.GetMetadata().Returns(new Metadata(fixture.Create())); + mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(mockedFeatureProvider); - var client = Api.Instance.GetClient(domain, clientVersion, mockedLogger); + await Api.Instance.SetProviderAsync(mockedFeatureProvider); + var client = Api.Instance.GetClient(domain, clientVersion, mockedLogger); - var evaluationDetails = await client.GetObjectDetailsAsync(flagName, defaultValue); - Assert.Equal(ErrorType.TypeMismatch, evaluationDetails.ErrorType); - Assert.Equal(new InvalidCastException().Message, evaluationDetails.ErrorMessage); + var evaluationDetails = await client.GetObjectDetailsAsync(flagName, defaultValue); + Assert.Equal(ErrorType.TypeMismatch, evaluationDetails.ErrorType); + Assert.Equal(new InvalidCastException().Message, evaluationDetails.ErrorMessage); - _ = mockedFeatureProvider.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); + _ = mockedFeatureProvider.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); - mockedLogger.Received(0).IsEnabled(LogLevel.Error); - } + mockedLogger.Received(0).IsEnabled(LogLevel.Error); + } - [Fact] - [Specification("1.7.3", "The client's provider status accessor MUST indicate READY if the initialize function of the associated provider terminates normally.")] - [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] - public async Task Provider_Status_Should_Be_Ready_If_Init_Succeeds() - { - var name = "1.7.3"; - // provider which succeeds initialization - var provider = new TestProvider(); - FeatureClient client = Api.Instance.GetClient(name); - Assert.Equal(ProviderStatus.NotReady, provider.Status); - await Api.Instance.SetProviderAsync(name, provider); - - // after init fails fatally, status should be READY - Assert.Equal(ProviderStatus.Ready, client.ProviderStatus); - } + [Fact] + [Specification("1.7.3", "The client's provider status accessor MUST indicate READY if the initialize function of the associated provider terminates normally.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Ready_If_Init_Succeeds() + { + var name = "1.7.3"; + // provider which succeeds initialization + var provider = new TestProvider(); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be READY + Assert.Equal(ProviderStatus.Ready, client.ProviderStatus); + } - [Fact] - [Specification("1.7.4", "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")] - [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] - public async Task Provider_Status_Should_Be_Error_If_Init_Fails() - { - var name = "1.7.4"; - // provider which fails initialization - var provider = new TestProvider("some-name", new GeneralException("fake")); - FeatureClient client = Api.Instance.GetClient(name); - Assert.Equal(ProviderStatus.NotReady, provider.Status); - await Api.Instance.SetProviderAsync(name, provider); - - // after init fails fatally, status should be ERROR - Assert.Equal(ProviderStatus.Error, client.ProviderStatus); - } + [Fact] + [Specification("1.7.4", "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Error_If_Init_Fails() + { + var name = "1.7.4"; + // provider which fails initialization + var provider = new TestProvider("some-name", new GeneralException("fake")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be ERROR + Assert.Equal(ProviderStatus.Error, client.ProviderStatus); + } - [Fact] - [Specification("1.7.5", "The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider terminates abnormally and indicates error code PROVIDER_FATAL.")] - [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] - public async Task Provider_Status_Should_Be_Fatal_If_Init_Fatal() - { - var name = "1.7.5"; - // provider which fails initialization fatally - var provider = new TestProvider(name, new ProviderFatalException("fatal")); - FeatureClient client = Api.Instance.GetClient(name); - Assert.Equal(ProviderStatus.NotReady, provider.Status); - await Api.Instance.SetProviderAsync(name, provider); - - // after init fails fatally, status should be FATAL - Assert.Equal(ProviderStatus.Fatal, client.ProviderStatus); - } + [Fact] + [Specification("1.7.5", "The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider terminates abnormally and indicates error code PROVIDER_FATAL.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Fatal_If_Init_Fatal() + { + var name = "1.7.5"; + // provider which fails initialization fatally + var provider = new TestProvider(name, new ProviderFatalException("fatal")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be FATAL + Assert.Equal(ProviderStatus.Fatal, client.ProviderStatus); + } - [Fact] - [Specification("1.7.6", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] - public async Task Must_Short_Circuit_Not_Ready() - { - var name = "1.7.6"; - var defaultStr = "123-default"; - - // provider which is never ready (ready after maxValue) - var provider = new TestProvider(name, null, int.MaxValue); - FeatureClient client = Api.Instance.GetClient(name); - Assert.Equal(ProviderStatus.NotReady, provider.Status); - _ = Api.Instance.SetProviderAsync(name, provider); - - var details = await client.GetStringDetailsAsync("some-flag", defaultStr); - Assert.Equal(defaultStr, details.Value); - Assert.Equal(ErrorType.ProviderNotReady, details.ErrorType); - Assert.Equal(Reason.Error, details.Reason); - } + [Fact] + [Specification("1.7.6", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] + public async Task Must_Short_Circuit_Not_Ready() + { + var name = "1.7.6"; + var defaultStr = "123-default"; + + // provider which is never ready (ready after maxValue) + var provider = new TestProvider(name, null, int.MaxValue); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + _ = Api.Instance.SetProviderAsync(name, provider); + + var details = await client.GetStringDetailsAsync("some-flag", defaultStr); + Assert.Equal(defaultStr, details.Value); + Assert.Equal(ErrorType.ProviderNotReady, details.ErrorType); + Assert.Equal(Reason.Error, details.Reason); + } - [Fact] - [Specification("1.7.7", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] - public async Task Must_Short_Circuit_Fatal() - { - var name = "1.7.6"; - var defaultStr = "456-default"; - - // provider which immediately fails fatally - var provider = new TestProvider(name, new ProviderFatalException("fake")); - FeatureClient client = Api.Instance.GetClient(name); - Assert.Equal(ProviderStatus.NotReady, provider.Status); - _ = Api.Instance.SetProviderAsync(name, provider); - - var details = await client.GetStringDetailsAsync("some-flag", defaultStr); - Assert.Equal(defaultStr, details.Value); - Assert.Equal(ErrorType.ProviderFatal, details.ErrorType); - Assert.Equal(Reason.Error, details.Reason); - } + [Fact] + [Specification("1.7.7", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] + public async Task Must_Short_Circuit_Fatal() + { + var name = "1.7.6"; + var defaultStr = "456-default"; + + // provider which immediately fails fatally + var provider = new TestProvider(name, new ProviderFatalException("fake")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + _ = Api.Instance.SetProviderAsync(name, provider); + + var details = await client.GetStringDetailsAsync("some-flag", defaultStr); + Assert.Equal(defaultStr, details.Value); + Assert.Equal(ErrorType.ProviderFatal, details.ErrorType); + Assert.Equal(Reason.Error, details.Reason); + } - [Fact] - public async Task Should_Resolve_BooleanValue() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + [Fact] + public async Task Should_Resolve_BooleanValue() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); - Assert.Equal(defaultValue, await client.GetBooleanValueAsync(flagName, defaultValue)); + Assert.Equal(defaultValue, await client.GetBooleanValueAsync(flagName, defaultValue)); - _ = featureProviderMock.Received(1).ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()); - } + _ = featureProviderMock.Received(1).ResolveBooleanValueAsync(flagName, defaultValue, Arg.Any()); + } - [Fact] - public async Task Should_Resolve_StringValue() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + [Fact] + public async Task Should_Resolve_StringValue() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStringValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStringValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); - Assert.Equal(defaultValue, await client.GetStringValueAsync(flagName, defaultValue)); + Assert.Equal(defaultValue, await client.GetStringValueAsync(flagName, defaultValue)); - _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultValue, Arg.Any()); - } + _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultValue, Arg.Any()); + } - [Fact] - public async Task Should_Resolve_IntegerValue() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + [Fact] + public async Task Should_Resolve_IntegerValue() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); - Assert.Equal(defaultValue, await client.GetIntegerValueAsync(flagName, defaultValue)); + Assert.Equal(defaultValue, await client.GetIntegerValueAsync(flagName, defaultValue)); - _ = featureProviderMock.Received(1).ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()); - } + _ = featureProviderMock.Received(1).ResolveIntegerValueAsync(flagName, defaultValue, Arg.Any()); + } - [Fact] - public async Task Should_Resolve_DoubleValue() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + [Fact] + public async Task Should_Resolve_DoubleValue() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); - Assert.Equal(defaultValue, await client.GetDoubleValueAsync(flagName, defaultValue)); + Assert.Equal(defaultValue, await client.GetDoubleValueAsync(flagName, defaultValue)); - _ = featureProviderMock.Received(1).ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()); - } + _ = featureProviderMock.Received(1).ResolveDoubleValueAsync(flagName, defaultValue, Arg.Any()); + } - [Fact] - public async Task Should_Resolve_StructureValue() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); + [Fact] + public async Task Should_Resolve_StructureValue() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(new ResolutionDetails(flagName, defaultValue)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); - Assert.Equal(defaultValue, await client.GetObjectValueAsync(flagName, defaultValue)); + Assert.Equal(defaultValue, await client.GetObjectValueAsync(flagName, defaultValue)); - _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); - } + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); + } - [Fact] - public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); - const string testMessage = "Couldn't parse flag data."; - - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, "ERROR", null, testMessage))); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); - var response = await client.GetObjectDetailsAsync(flagName, defaultValue); - - Assert.Equal(ErrorType.ParseError, response.ErrorType); - Assert.Equal(Reason.Error, response.Reason); - Assert.Equal(testMessage, response.ErrorMessage); - _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); - } + [Fact] + public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + const string testMessage = "Couldn't parse flag data."; + + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, "ERROR", null, testMessage))); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); + + Assert.Equal(ErrorType.ParseError, response.ErrorType); + Assert.Equal(Reason.Error, response.Reason); + Assert.Equal(testMessage, response.ErrorMessage); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); + } - [Fact] - public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); - const string testMessage = "Couldn't parse flag data."; - - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); - var response = await client.GetObjectDetailsAsync(flagName, defaultValue); - - Assert.Equal(ErrorType.ParseError, response.ErrorType); - Assert.Equal(Reason.Error, response.Reason); - Assert.Equal(testMessage, response.ErrorMessage); - _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); - } + [Fact] + public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + const string testMessage = "Couldn't parse flag data."; + + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()).Throws(new FeatureProviderException(ErrorType.ParseError, testMessage)); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); + + Assert.Equal(ErrorType.ParseError, response.ErrorType); + Assert.Equal(Reason.Error, response.Reason); + Assert.Equal(testMessage, response.ErrorMessage); + _ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); + } - [Fact] - public async Task When_Error_Is_Returned_From_Provider_Should_Not_Run_After_Hook_But_Error_Hook() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); - const string testMessage = "Couldn't parse flag data."; - - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()) - .Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, - "ERROR", null, testMessage))); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - - await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); - var testHook = new TestHook(); - client.AddHooks(testHook); - var response = await client.GetObjectDetailsAsync(flagName, defaultValue); - - Assert.Equal(ErrorType.ParseError, response.ErrorType); - Assert.Equal(Reason.Error, response.Reason); - Assert.Equal(testMessage, response.ErrorMessage); - _ = featureProviderMock.Received(1) - .ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); - - Assert.Equal(1, testHook.BeforeCallCount); - Assert.Equal(0, testHook.AfterCallCount); - Assert.Equal(1, testHook.ErrorCallCount); - Assert.Equal(1, testHook.FinallyCallCount); - } + [Fact] + public async Task When_Error_Is_Returned_From_Provider_Should_Not_Run_After_Hook_But_Error_Hook() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + const string testMessage = "Couldn't parse flag data."; + + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()) + .Returns(Task.FromResult(new ResolutionDetails(flagName, defaultValue, ErrorType.ParseError, + "ERROR", null, testMessage))); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + + await Api.Instance.SetProviderAsync(featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); + var testHook = new TestHook(); + client.AddHooks(testHook); + var response = await client.GetObjectDetailsAsync(flagName, defaultValue); + + Assert.Equal(ErrorType.ParseError, response.ErrorType); + Assert.Equal(Reason.Error, response.Reason); + Assert.Equal(testMessage, response.ErrorMessage); + _ = featureProviderMock.Received(1) + .ResolveStructureValueAsync(flagName, defaultValue, Arg.Any()); + + Assert.Equal(1, testHook.BeforeCallCount); + Assert.Equal(0, testHook.AfterCallCount); + Assert.Equal(1, testHook.ErrorCallCount); + Assert.Equal(1, testHook.FinallyCallCount); + } - [Fact] - public async Task Cancellation_Token_Added_Is_Passed_To_Provider() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultString = fixture.Create(); - var cancelledReason = "cancelled"; + [Fact] + public async Task Cancellation_Token_Added_Is_Passed_To_Provider() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultString = fixture.Create(); + var cancelledReason = "cancelled"; - var cts = new CancellationTokenSource(); + var cts = new CancellationTokenSource(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => + { + var token = args.ArgAt(3); + while (!token.IsCancellationRequested) { - var token = args.ArgAt(3); - while (!token.IsCancellationRequested) - { - await Task.Delay(10); // artificially delay until cancelled - } + await Task.Delay(10); // artificially delay until cancelled + } - return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); - }); - featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); - featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); + return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); + }); + featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); + featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(domain, featureProviderMock); - var client = Api.Instance.GetClient(domain, clientVersion); - var task = client.GetStringDetailsAsync(flagName, defaultString, EvaluationContext.Empty, null, cts.Token); - cts.Cancel(); // cancel before awaiting + await Api.Instance.SetProviderAsync(domain, featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); + var task = client.GetStringDetailsAsync(flagName, defaultString, EvaluationContext.Empty, null, cts.Token); + cts.Cancel(); // cancel before awaiting - var response = await task; - Assert.Equal(defaultString, response.Value); - Assert.Equal(cancelledReason, response.Reason); - _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultString, Arg.Any(), cts.Token); - } + var response = await task; + Assert.Equal(defaultString, response.Value); + Assert.Equal(cancelledReason, response.Reason); + _ = featureProviderMock.Received(1).ResolveStringValueAsync(flagName, defaultString, Arg.Any(), cts.Token); + } - [Fact] - public void Should_Get_And_Set_Context() - { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var KEY = "key"; - var VAL = 1; - FeatureClient client = Api.Instance.GetClient(domain, clientVersion); - client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); - Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); - } + [Fact] + public void Should_Get_And_Set_Context() + { + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var KEY = "key"; + var VAL = 1; + FeatureClient client = Api.Instance.GetClient(domain, clientVersion); + client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); + Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); + } - [Fact] - public void ToFlagEvaluationDetails_Should_Convert_All_Properties() - { - var fixture = new Fixture(); - var flagName = fixture.Create(); - var boolValue = fixture.Create(); - var errorType = fixture.Create(); - var reason = fixture.Create(); - var variant = fixture.Create(); - var errorMessage = fixture.Create(); - var flagData = fixture - .CreateMany>(10) - .ToDictionary(x => x.Key, x => x.Value); - var flagMetadata = new ImmutableMetadata(flagData); - - var expected = new ResolutionDetails(flagName, boolValue, errorType, reason, variant, errorMessage, flagMetadata); - var result = expected.ToFlagEvaluationDetails(); - - Assert.Equivalent(expected, result); - } + [Fact] + public void ToFlagEvaluationDetails_Should_Convert_All_Properties() + { + var fixture = new Fixture(); + var flagName = fixture.Create(); + var boolValue = fixture.Create(); + var errorType = fixture.Create(); + var reason = fixture.Create(); + var variant = fixture.Create(); + var errorMessage = fixture.Create(); + var flagData = fixture + .CreateMany>(10) + .ToDictionary(x => x.Key, x => x.Value); + var flagMetadata = new ImmutableMetadata(flagData); + + var expected = new ResolutionDetails(flagName, boolValue, errorType, reason, variant, errorMessage, flagMetadata); + var result = expected.ToFlagEvaluationDetails(); + + Assert.Equivalent(expected, result); + } - [Fact] - [Specification("6.1.1", "The `client` MUST define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required), `evaluation context` (optional) and `tracking event details` (optional), which returns nothing.")] - [Specification("6.1.2", "The `client` MUST define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required) and `tracking event details` (optional), which returns nothing.")] - [Specification("6.1.4", "If the client's `track` function is called and the associated provider does not implement tracking, the client's `track` function MUST no-op.")] - public async Task TheClient_ImplementsATrackingFunction() - { - var provider = new TestProvider(); - await Api.Instance.SetProviderAsync(provider); - var client = Api.Instance.GetClient(); - - const string trackingEventName = "trackingEventName"; - var trackingEventDetails = new TrackingEventDetailsBuilder().Build(); - client.Track(trackingEventName); - client.Track(trackingEventName, EvaluationContext.Empty); - client.Track(trackingEventName, EvaluationContext.Empty, trackingEventDetails); - client.Track(trackingEventName, trackingEventDetails: trackingEventDetails); - - Assert.Equal(4, provider.GetTrackingInvocations().Count); - Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[0].Item1); - Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[1].Item1); - Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[2].Item1); - Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[3].Item1); - - Assert.NotNull(provider.GetTrackingInvocations()[0].Item2); - Assert.NotNull(provider.GetTrackingInvocations()[1].Item2); - Assert.NotNull(provider.GetTrackingInvocations()[2].Item2); - Assert.NotNull(provider.GetTrackingInvocations()[3].Item2); - - Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[0].Item2!.Count); - Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[1].Item2!.Count); - Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[2].Item2!.Count); - Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[3].Item2!.Count); - - Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[0].Item2!.TargetingKey); - Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[1].Item2!.TargetingKey); - Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[2].Item2!.TargetingKey); - Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[3].Item2!.TargetingKey); - - Assert.Null(provider.GetTrackingInvocations()[0].Item3); - Assert.Null(provider.GetTrackingInvocations()[1].Item3); - Assert.Equal(trackingEventDetails, provider.GetTrackingInvocations()[2].Item3); - Assert.Equal(trackingEventDetails, provider.GetTrackingInvocations()[3].Item3); - } + [Fact] + [Specification("6.1.1", "The `client` MUST define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required), `evaluation context` (optional) and `tracking event details` (optional), which returns nothing.")] + [Specification("6.1.2", "The `client` MUST define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required) and `tracking event details` (optional), which returns nothing.")] + [Specification("6.1.4", "If the client's `track` function is called and the associated provider does not implement tracking, the client's `track` function MUST no-op.")] + public async Task TheClient_ImplementsATrackingFunction() + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); + + const string trackingEventName = "trackingEventName"; + var trackingEventDetails = new TrackingEventDetailsBuilder().Build(); + client.Track(trackingEventName); + client.Track(trackingEventName, EvaluationContext.Empty); + client.Track(trackingEventName, EvaluationContext.Empty, trackingEventDetails); + client.Track(trackingEventName, trackingEventDetails: trackingEventDetails); + + Assert.Equal(4, provider.GetTrackingInvocations().Count); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[0].Item1); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[1].Item1); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[2].Item1); + Assert.Equal(trackingEventName, provider.GetTrackingInvocations()[3].Item1); + + Assert.NotNull(provider.GetTrackingInvocations()[0].Item2); + Assert.NotNull(provider.GetTrackingInvocations()[1].Item2); + Assert.NotNull(provider.GetTrackingInvocations()[2].Item2); + Assert.NotNull(provider.GetTrackingInvocations()[3].Item2); + + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[0].Item2!.Count); + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[1].Item2!.Count); + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[2].Item2!.Count); + Assert.Equal(EvaluationContext.Empty.Count, provider.GetTrackingInvocations()[3].Item2!.Count); + + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[0].Item2!.TargetingKey); + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[1].Item2!.TargetingKey); + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[2].Item2!.TargetingKey); + Assert.Equal(EvaluationContext.Empty.TargetingKey, provider.GetTrackingInvocations()[3].Item2!.TargetingKey); + + Assert.Null(provider.GetTrackingInvocations()[0].Item3); + Assert.Null(provider.GetTrackingInvocations()[1].Item3); + Assert.Equal(trackingEventDetails, provider.GetTrackingInvocations()[2].Item3); + Assert.Equal(trackingEventDetails, provider.GetTrackingInvocations()[3].Item3); + } - [Fact] - public async Task PassingAnEmptyStringAsTrackingEventName_ThrowsArgumentException() - { - var provider = new TestProvider(); - await Api.Instance.SetProviderAsync(provider); - var client = Api.Instance.GetClient(); + [Fact] + public async Task PassingAnEmptyStringAsTrackingEventName_ThrowsArgumentException() + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); - Assert.Throws(() => client.Track("")); - } + Assert.Throws(() => client.Track("")); + } - [Fact] - public async Task PassingABlankStringAsTrackingEventName_ThrowsArgumentException() - { - var provider = new TestProvider(); - await Api.Instance.SetProviderAsync(provider); - var client = Api.Instance.GetClient(); + [Fact] + public async Task PassingABlankStringAsTrackingEventName_ThrowsArgumentException() + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); - Assert.Throws(() => client.Track(" \n ")); - } + Assert.Throws(() => client.Track(" \n ")); + } - public static TheoryData GenerateMergeEvaluationContextTestData() + public static TheoryData GenerateMergeEvaluationContextTestData() + { + const string key = "key"; + const string global = "global"; + const string client = "client"; + const string invocation = "invocation"; + var globalEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "global").Build(), null }; + var clientEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "client").Build(), null }; + var invocationEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "invocation").Build(), null }; + + var data = new TheoryData(); + for (int i = 0; i < 2; i++) { - const string key = "key"; - const string global = "global"; - const string client = "client"; - const string invocation = "invocation"; - var globalEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "global").Build(), null }; - var clientEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "client").Build(), null }; - var invocationEvaluationContext = new[] { new EvaluationContextBuilder().Set(key, "invocation").Build(), null }; - - var data = new TheoryData(); - for (int i = 0; i < 2; i++) + for (int j = 0; j < 2; j++) { - for (int j = 0; j < 2; j++) + for (int k = 0; k < 2; k++) { - for (int k = 0; k < 2; k++) - { - if (i == 1 && j == 1 && k == 1) continue; - string expected; - if (k == 0) expected = invocation; - else if (j == 0) expected = client; - else expected = global; - data.Add(key, globalEvaluationContext[i], clientEvaluationContext[j], invocationEvaluationContext[k], expected); - } + if (i == 1 && j == 1 && k == 1) continue; + string expected; + if (k == 0) expected = invocation; + else if (j == 0) expected = client; + else expected = global; + data.Add(key, globalEvaluationContext[i], clientEvaluationContext[j], invocationEvaluationContext[k], expected); } } - - return data; } - [Theory] - [MemberData(nameof(GenerateMergeEvaluationContextTestData))] - [Specification("6.1.3", "The evaluation context passed to the provider's track function MUST be merged in the order: API (global; lowest precedence) - transaction - client - invocation (highest precedence), with duplicate values being overwritten.")] - public async Task TheClient_MergesTheEvaluationContextInTheCorrectOrder(string key, EvaluationContext? globalEvaluationContext, EvaluationContext? clientEvaluationContext, EvaluationContext? invocationEvaluationContext, string expectedResult) - { - var provider = new TestProvider(); - await Api.Instance.SetProviderAsync(provider); - var client = Api.Instance.GetClient(); + return data; + } - const string trackingEventName = "trackingEventName"; + [Theory] + [MemberData(nameof(GenerateMergeEvaluationContextTestData))] + [Specification("6.1.3", "The evaluation context passed to the provider's track function MUST be merged in the order: API (global; lowest precedence) - transaction - client - invocation (highest precedence), with duplicate values being overwritten.")] + public async Task TheClient_MergesTheEvaluationContextInTheCorrectOrder(string key, EvaluationContext? globalEvaluationContext, EvaluationContext? clientEvaluationContext, EvaluationContext? invocationEvaluationContext, string expectedResult) + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); - Api.Instance.SetContext(globalEvaluationContext); - client.SetContext(clientEvaluationContext); - client.Track(trackingEventName, invocationEvaluationContext); - Assert.Single(provider.GetTrackingInvocations()); - var actualEvaluationContext = provider.GetTrackingInvocations()[0].Item2; - Assert.NotNull(actualEvaluationContext); - Assert.NotEqual(0, actualEvaluationContext.Count); + const string trackingEventName = "trackingEventName"; - Assert.Equal(expectedResult, actualEvaluationContext.GetValue(key).AsString); - } + Api.Instance.SetContext(globalEvaluationContext); + client.SetContext(clientEvaluationContext); + client.Track(trackingEventName, invocationEvaluationContext); + Assert.Single(provider.GetTrackingInvocations()); + var actualEvaluationContext = provider.GetTrackingInvocations()[0].Item2; + Assert.NotNull(actualEvaluationContext); + Assert.NotEqual(0, actualEvaluationContext.Count); - [Fact] - [Specification("4.3.8", "'evaluation details' passed to the 'finally' stage matches the evaluation details returned to the application author")] - public async Task FinallyHook_IncludesEvaluationDetails() - { - // Arrange - var provider = new TestProvider(); - var providerHook = Substitute.For(); - provider.AddHook(providerHook); - await Api.Instance.SetProviderAsync(provider); - var client = Api.Instance.GetClient(); + Assert.Equal(expectedResult, actualEvaluationContext.GetValue(key).AsString); + } - const string flagName = "flagName"; + [Fact] + [Specification("4.3.8", "'evaluation details' passed to the 'finally' stage matches the evaluation details returned to the application author")] + public async Task FinallyHook_IncludesEvaluationDetails() + { + // Arrange + var provider = new TestProvider(); + var providerHook = Substitute.For(); + provider.AddHook(providerHook); + await Api.Instance.SetProviderAsync(provider); + var client = Api.Instance.GetClient(); - // Act - var evaluationDetails = await client.GetBooleanDetailsAsync(flagName, true); + const string flagName = "flagName"; - // Assert - await providerHook.Received(1).FinallyAsync(Arg.Any>(), evaluationDetails); - } + // Act + var evaluationDetails = await client.GetBooleanDetailsAsync(flagName, true); + + // Assert + await providerHook.Received(1).FinallyAsync(Arg.Any>(), evaluationDetails); } } diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 20b0ec2e..630ec435 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -5,218 +5,217 @@ using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +public class OpenFeatureEvaluationContextTests { - public class OpenFeatureEvaluationContextTests + [Fact] + public void Should_Merge_Two_Contexts() { - [Fact] - public void Should_Merge_Two_Contexts() - { - var contextBuilder1 = new EvaluationContextBuilder() - .Set("key1", "value1"); - var contextBuilder2 = new EvaluationContextBuilder() - .Set("key2", "value2"); - var context1 = contextBuilder1.Merge(contextBuilder2.Build()).Build(); - - Assert.Equal(2, context1.Count); - Assert.Equal("value1", context1.GetValue("key1").AsString); - Assert.Equal("value2", context1.GetValue("key2").AsString); - } + var contextBuilder1 = new EvaluationContextBuilder() + .Set("key1", "value1"); + var contextBuilder2 = new EvaluationContextBuilder() + .Set("key2", "value2"); + var context1 = contextBuilder1.Merge(contextBuilder2.Build()).Build(); + + Assert.Equal(2, context1.Count); + Assert.Equal("value1", context1.GetValue("key1").AsString); + Assert.Equal("value2", context1.GetValue("key2").AsString); + } - [Fact] - public void Should_Change_TargetingKey_From_OverridingContext() - { - var contextBuilder1 = new EvaluationContextBuilder() - .Set("key1", "value1") - .SetTargetingKey("targeting_key"); - var contextBuilder2 = new EvaluationContextBuilder() - .Set("key2", "value2") - .SetTargetingKey("overriding_key"); + [Fact] + public void Should_Change_TargetingKey_From_OverridingContext() + { + var contextBuilder1 = new EvaluationContextBuilder() + .Set("key1", "value1") + .SetTargetingKey("targeting_key"); + var contextBuilder2 = new EvaluationContextBuilder() + .Set("key2", "value2") + .SetTargetingKey("overriding_key"); - var mergeContext = contextBuilder1.Merge(contextBuilder2.Build()).Build(); + var mergeContext = contextBuilder1.Merge(contextBuilder2.Build()).Build(); - Assert.Equal("overriding_key", mergeContext.TargetingKey); - } + Assert.Equal("overriding_key", mergeContext.TargetingKey); + } - [Fact] - public void Should_Retain_TargetingKey_When_OverridingContext_TargetingKey_Value_IsEmpty() - { - var contextBuilder1 = new EvaluationContextBuilder() - .Set("key1", "value1") - .SetTargetingKey("targeting_key"); - var contextBuilder2 = new EvaluationContextBuilder() - .Set("key2", "value2"); + [Fact] + public void Should_Retain_TargetingKey_When_OverridingContext_TargetingKey_Value_IsEmpty() + { + var contextBuilder1 = new EvaluationContextBuilder() + .Set("key1", "value1") + .SetTargetingKey("targeting_key"); + var contextBuilder2 = new EvaluationContextBuilder() + .Set("key2", "value2"); - var mergeContext = contextBuilder1.Merge(contextBuilder2.Build()).Build(); + var mergeContext = contextBuilder1.Merge(contextBuilder2.Build()).Build(); - Assert.Equal("targeting_key", mergeContext.TargetingKey); - } + Assert.Equal("targeting_key", mergeContext.TargetingKey); + } - [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.")] - public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Context() - { - var contextBuilder1 = new EvaluationContextBuilder(); - var contextBuilder2 = new EvaluationContextBuilder(); + [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.")] + public void Should_Merge_TwoContexts_And_Override_Duplicates_With_RightHand_Context() + { + var contextBuilder1 = new EvaluationContextBuilder(); + var contextBuilder2 = new EvaluationContextBuilder(); - contextBuilder1.Set("key1", "value1"); - contextBuilder2.Set("key1", "overriden_value"); - contextBuilder2.Set("key2", "value2"); + contextBuilder1.Set("key1", "value1"); + contextBuilder2.Set("key1", "overriden_value"); + contextBuilder2.Set("key2", "value2"); - var context1 = contextBuilder1.Merge(contextBuilder2.Build()).Build(); + var context1 = contextBuilder1.Merge(contextBuilder2.Build()).Build(); - Assert.Equal(2, context1.Count); - Assert.Equal("overriden_value", context1.GetValue("key1").AsString); - Assert.Equal("value2", context1.GetValue("key2").AsString); - } + Assert.Equal(2, context1.Count); + Assert.Equal("overriden_value", context1.GetValue("key1").AsString); + Assert.Equal("value2", context1.GetValue("key2").AsString); + } - [Fact] - [Specification("3.1.1", "The `evaluation context` structure MUST define an optional `targeting key` field of type string, identifying the subject of the flag evaluation.")] - [Specification("3.1.2", "The evaluation context MUST support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | datetime | structure`.")] - public void EvaluationContext_Should_All_Types() - { - var fixture = new Fixture(); - var now = fixture.Create(); - var structure = fixture.Create(); - var contextBuilder = new EvaluationContextBuilder() - .SetTargetingKey("targeting_key") - .Set("targeting_key", "userId") - .Set("key1", "value") - .Set("key2", 1) - .Set("key3", true) - .Set("key4", now) - .Set("key5", structure) - .Set("key6", 1.0); - - var context = contextBuilder.Build(); - - Assert.Equal("targeting_key", context.TargetingKey); - var targetingKeyValue = context.GetValue(context.TargetingKey!); - Assert.True(targetingKeyValue.IsString); - Assert.Equal("userId", targetingKeyValue.AsString); - - var value1 = context.GetValue("key1"); - Assert.True(value1.IsString); - Assert.Equal("value", value1.AsString); - - var value2 = context.GetValue("key2"); - Assert.True(value2.IsNumber); - Assert.Equal(1, value2.AsInteger); - - var value3 = context.GetValue("key3"); - Assert.True(value3.IsBoolean); - Assert.True(value3.AsBoolean); - - var value4 = context.GetValue("key4"); - Assert.True(value4.IsDateTime); - Assert.Equal(now, value4.AsDateTime); - - var value5 = context.GetValue("key5"); - Assert.True(value5.IsStructure); - Assert.Equal(structure, value5.AsStructure); - - var value6 = context.GetValue("key6"); - Assert.True(value6.IsNumber); - Assert.Equal(1.0, value6.AsDouble); - } + [Fact] + [Specification("3.1.1", "The `evaluation context` structure MUST define an optional `targeting key` field of type string, identifying the subject of the flag evaluation.")] + [Specification("3.1.2", "The evaluation context MUST support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | datetime | structure`.")] + public void EvaluationContext_Should_All_Types() + { + var fixture = new Fixture(); + var now = fixture.Create(); + var structure = fixture.Create(); + var contextBuilder = new EvaluationContextBuilder() + .SetTargetingKey("targeting_key") + .Set("targeting_key", "userId") + .Set("key1", "value") + .Set("key2", 1) + .Set("key3", true) + .Set("key4", now) + .Set("key5", structure) + .Set("key6", 1.0); + + var context = contextBuilder.Build(); + + Assert.Equal("targeting_key", context.TargetingKey); + var targetingKeyValue = context.GetValue(context.TargetingKey!); + Assert.True(targetingKeyValue.IsString); + Assert.Equal("userId", targetingKeyValue.AsString); + + var value1 = context.GetValue("key1"); + Assert.True(value1.IsString); + Assert.Equal("value", value1.AsString); + + var value2 = context.GetValue("key2"); + Assert.True(value2.IsNumber); + Assert.Equal(1, value2.AsInteger); + + var value3 = context.GetValue("key3"); + Assert.True(value3.IsBoolean); + Assert.True(value3.AsBoolean); + + var value4 = context.GetValue("key4"); + Assert.True(value4.IsDateTime); + Assert.Equal(now, value4.AsDateTime); + + var value5 = context.GetValue("key5"); + Assert.True(value5.IsStructure); + Assert.Equal(structure, value5.AsStructure); + + var value6 = context.GetValue("key6"); + Assert.True(value6.IsNumber); + Assert.Equal(1.0, value6.AsDouble); + } - [Fact] - [Specification("3.1.4", "The evaluation context fields MUST have an unique key.")] - public void When_Duplicate_Key_Set_It_Replaces_Value() - { - var contextBuilder = new EvaluationContextBuilder().Set("key", "value"); - contextBuilder.Set("key", "overriden_value"); - Assert.Equal("overriden_value", contextBuilder.Build().GetValue("key").AsString); - } + [Fact] + [Specification("3.1.4", "The evaluation context fields MUST have an unique key.")] + public void When_Duplicate_Key_Set_It_Replaces_Value() + { + var contextBuilder = new EvaluationContextBuilder().Set("key", "value"); + contextBuilder.Set("key", "overriden_value"); + Assert.Equal("overriden_value", contextBuilder.Build().GetValue("key").AsString); + } - [Fact] - [Specification("3.1.3", "The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")] - public void Should_Be_Able_To_Get_All_Values() + [Fact] + [Specification("3.1.3", "The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")] + public void Should_Be_Able_To_Get_All_Values() + { + var context = new EvaluationContextBuilder() + .Set("key1", "value1") + .Set("key2", "value2") + .Set("key3", "value3") + .Set("key4", "value4") + .Set("key5", "value5") + .Build(); + + // Iterate over key value pairs and check consistency + var count = 0; + foreach (var keyValue in context) { - var context = new EvaluationContextBuilder() - .Set("key1", "value1") - .Set("key2", "value2") - .Set("key3", "value3") - .Set("key4", "value4") - .Set("key5", "value5") - .Build(); - - // Iterate over key value pairs and check consistency - var count = 0; - foreach (var keyValue in context) - { - Assert.Equal(keyValue.Value.AsString, context.GetValue(keyValue.Key).AsString); - count++; - } - - Assert.Equal(count, context.Count); + Assert.Equal(keyValue.Value.AsString, context.GetValue(keyValue.Key).AsString); + count++; } - [Fact] - public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() - { - // Arrange - var key = "testKey"; - var expectedValue = new Value("testValue"); - var structure = new Structure(new Dictionary { { key, expectedValue } }); - var evaluationContext = new EvaluationContext(structure); - - // Act - var result = evaluationContext.TryGetValue(key, out var actualValue); - - // Assert - Assert.True(result); - Assert.Equal(expectedValue, actualValue); - } + Assert.Equal(count, context.Count); + } - [Fact] - public void GetValueOnTargetingKeySetWithTargetingKey_Equals_TargetingKey() - { - // Arrange - var value = "my_targeting_key"; - var evaluationContext = EvaluationContext.Builder().SetTargetingKey(value).Build(); - - // Act - var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); - var actualFromTargetingKey = evaluationContext.TargetingKey; - - // Assert - Assert.True(result); - Assert.Equal(value, actualFromStructure?.AsString); - Assert.Equal(value, actualFromTargetingKey); - } + [Fact] + public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() + { + // Arrange + var key = "testKey"; + var expectedValue = new Value("testValue"); + var structure = new Structure(new Dictionary { { key, expectedValue } }); + var evaluationContext = new EvaluationContext(structure); + + // Act + var result = evaluationContext.TryGetValue(key, out var actualValue); + + // Assert + Assert.True(result); + Assert.Equal(expectedValue, actualValue); + } - [Fact] - public void GetValueOnTargetingKeySetWithStructure_Equals_TargetingKey() - { - // Arrange - var value = "my_targeting_key"; - var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(value)).Build(); - - // Act - var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); - var actualFromTargetingKey = evaluationContext.TargetingKey; - - // Assert - Assert.True(result); - Assert.Equal(value, actualFromStructure?.AsString); - Assert.Equal(value, actualFromTargetingKey); - } + [Fact] + public void GetValueOnTargetingKeySetWithTargetingKey_Equals_TargetingKey() + { + // Arrange + var value = "my_targeting_key"; + var evaluationContext = EvaluationContext.Builder().SetTargetingKey(value).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Equal(value, actualFromStructure?.AsString); + Assert.Equal(value, actualFromTargetingKey); + } - [Fact] - public void GetValueOnTargetingKeySetWithNonStringValue_Equals_Null() - { - // Arrange - var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(1)).Build(); + [Fact] + public void GetValueOnTargetingKeySetWithStructure_Equals_TargetingKey() + { + // Arrange + var value = "my_targeting_key"; + var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(value)).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Equal(value, actualFromStructure?.AsString); + Assert.Equal(value, actualFromTargetingKey); + } - // Act - var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); - var actualFromTargetingKey = evaluationContext.TargetingKey; + [Fact] + public void GetValueOnTargetingKeySetWithNonStringValue_Equals_Null() + { + // Arrange + var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(1)).Build(); - // Assert - Assert.True(result); - Assert.Null(actualFromStructure?.AsString); - Assert.Null(actualFromTargetingKey); - } + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Null(actualFromStructure?.AsString); + Assert.Null(actualFromTargetingKey); } } diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index a4b0d111..c8cea92b 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -10,511 +10,510 @@ using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class OpenFeatureEventTest : ClearOpenFeatureInstanceFixture { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class OpenFeatureEventTest : ClearOpenFeatureInstanceFixture + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() { - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() - { - var eventHandler = Substitute.For(); + var eventHandler = Substitute.For(); - var eventExecutor = new EventExecutor(); + var eventExecutor = new EventExecutor(); - eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); + eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); - var eventMetadata = new ImmutableMetadata(new Dictionary { { "foo", "bar" } }); - var myEvent = new Event + var eventMetadata = new ImmutableMetadata(new Dictionary { { "foo", "bar" } }); + var myEvent = new Event + { + EventPayload = new ProviderEventPayload { - EventPayload = new ProviderEventPayload + Type = ProviderEventTypes.ProviderConfigurationChanged, + Message = "The provider is ready", + EventMetadata = eventMetadata, + FlagsChanged = new List { - Type = ProviderEventTypes.ProviderConfigurationChanged, - Message = "The provider is ready", - EventMetadata = eventMetadata, - FlagsChanged = new List - { - "flag1", "flag2" - } + "flag1", "flag2" } - }; - eventExecutor.EventChannel.Writer.TryWrite(myEvent); + } + }; + eventExecutor.EventChannel.Writer.TryWrite(myEvent); - Thread.Sleep(1000); + Thread.Sleep(1000); - eventHandler.Received().Invoke(Arg.Is(payload => payload == myEvent.EventPayload)); + eventHandler.Received().Invoke(Arg.Is(payload => payload == myEvent.EventPayload)); - // shut down the event executor - await eventExecutor.ShutdownAsync(); + // shut down the event executor + await eventExecutor.ShutdownAsync(); - // the next event should not be propagated to the event handler - var newEventPayload = new ProviderEventPayload - { - Type = ProviderEventTypes.ProviderStale - }; + // the next event should not be propagated to the event handler + var newEventPayload = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderStale + }; - eventExecutor.EventChannel.Writer.TryWrite(newEventPayload); + eventExecutor.EventChannel.Writer.TryWrite(newEventPayload); - eventHandler.DidNotReceive().Invoke(newEventPayload); + eventHandler.DidNotReceive().Invoke(newEventPayload); - eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.Type == ProviderEventTypes.ProviderStale)); - } + eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.Type == ProviderEventTypes.ProviderStale)); + } - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - public async Task API_Level_Event_Handlers_Should_Be_Registered() - { - var eventHandler = Substitute.For(); - - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); - Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); - Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); - - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(testProvider); - - await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); - await testProvider.SendEventAsync(ProviderEventTypes.ProviderError); - await testProvider.SendEventAsync(ProviderEventTypes.ProviderStale); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady - ))); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged - ))); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError - ))); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale - ))); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] - [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] - public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Ready() - { - var eventHandler = Substitute.For(); - - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(testProvider); - - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady - ))); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] - [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] - public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Ready_Sync() - { - var eventHandler = Substitute.For(); - - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(testProvider); - - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady - ))); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.3.2", "If the provider's `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.")] - [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] - public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_After_Registering_Provider_Error() - { - var eventHandler = Substitute.For(); - - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(testProvider); - - testProvider.Status = ProviderStatus.Error; - - Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError - ))); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] - public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_After_Registering_Provider_Stale() - { - var eventHandler = Substitute.For(); - - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(testProvider); - - testProvider.Status = ProviderStatus.Stale; - - Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); - - await Utils.AssertUntilAsync(_ => eventHandler - .Received() - .Invoke( - Arg.Is( - payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale - ))); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")] - public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() - { - var eventHandler = Substitute.For(); + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + public async Task API_Level_Event_Handlers_Should_Be_Registered() + { + var eventHandler = Substitute.For(); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(testProvider); + + await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderError); + await testProvider.SendEventAsync(ProviderEventTypes.ProviderStale); + + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady + ))); + + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged + ))); + + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError + ))); + + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale + ))); + } - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Ready() + { + var eventHandler = Substitute.For(); - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(testProvider); + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(testProvider); - await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - var newTestProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(newTestProvider); + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady + ))); + } - await newTestProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Ready_Sync() + { + var eventHandler = Substitute.For(); - await Utils.AssertUntilAsync( - _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)) - ); - await Utils.AssertUntilAsync( - _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) - ); - } + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(testProvider); - [Fact] - [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")] - public async Task API_Level_Event_Handlers_Should_Be_Removable() - { - var eventHandler = Substitute.For(); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady + ))); + } - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(testProvider); + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.2", "If the provider's `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_After_Registering_Provider_Error() + { + var eventHandler = Substitute.For(); - Thread.Sleep(1000); - Api.Instance.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(testProvider); - var newTestProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(newTestProvider); + testProvider.Status = ProviderStatus.Error; - eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - } + Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] - public async Task API_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() - { - var fixture = new Fixture(); - - var failingEventHandler = Substitute.For(); - var eventHandler = Substitute.For(); - - failingEventHandler.When(x => x.Invoke(Arg.Any())) - .Do(x => throw new Exception()); - - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - - var testProvider = new TestProvider(fixture.Create()); - await Api.Instance.SetProviderAsync(testProvider); - - await Utils.AssertUntilAsync( - _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) - ); - await Utils.AssertUntilAsync( - _ => eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) - ); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - public async Task Client_Level_Event_Handlers_Should_Be_Registered() - { - var fixture = new Fixture(); - var eventHandler = Substitute.For(); + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderError + ))); + } - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var myClient = Api.Instance.GetClient(domain, clientVersion); + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_After_Registering_Provider_Stale() + { + var eventHandler = Substitute.For(); - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(testProvider); - myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + testProvider.Status = ProviderStatus.Stale; - eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - } + Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] - public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() - { - var fixture = new Fixture(); - - var failingEventHandler = Substitute.For(); - var eventHandler = Substitute.For(); - - failingEventHandler.When(x => x.Invoke(Arg.Any())) - .Do(x => throw new Exception()); - - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var myClient = Api.Instance.GetClient(domain, clientVersion); - - myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); - myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); - - await Utils.AssertUntilAsync( - _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) - ); - await Utils.AssertUntilAsync( - _ => eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) - ); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] - [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Provider() - { - var fixture = new Fixture(); - var eventHandler = Substitute.For(); - var clientEventHandler = Substitute.For(); - - var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); - var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); - - var apiProvider = new TestProvider(fixture.Create()); - var clientProvider = new TestProvider(fixture.Create()); - - // set the default provider on API level, but not specifically to the client - await Api.Instance.SetProviderAsync(apiProvider); - // set the other provider specifically for the client - await Api.Instance.SetProviderAsync(myClientWithBoundProvider.GetMetadata().Name!, clientProvider); - - myClientWithNoBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - myClientWithBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, clientEventHandler); - - eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name)); - eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name)); - - clientEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name)); - clientEventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name)); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] - [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")] - public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Named_Provider_Instead_of_Default() - { - var fixture = new Fixture(); - var clientEventHandler = Substitute.For(); - - var client = Api.Instance.GetClient(fixture.Create(), fixture.Create()); - - var defaultProvider = new TestProvider(fixture.Create()); - var clientProvider = new TestProvider(fixture.Create()); - - // set the default provider - await Api.Instance.SetProviderAsync(defaultProvider); - - client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler); - - await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); - - // verify that the client received the event from the default provider as there is no named provider registered yet - await Utils.AssertUntilAsync( - _ => clientEventHandler.Received(1) - .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) - ); - - // set the other provider specifically for the client - await Api.Instance.SetProviderAsync(client.GetMetadata().Name!, clientProvider); - - // now, send another event for the default handler - await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); - await clientProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); - - // now the client should have received only the event from the named provider - await Utils.AssertUntilAsync( - _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) - ); - // for the default provider, the number of received events should stay unchanged - await Utils.AssertUntilAsync( - _ => clientEventHandler.Received(1) - .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) - ); - } - - [Fact] - [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] - [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] - [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] - public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering() - { - var fixture = new Fixture(); - var eventHandler = Substitute.For(); + await Utils.AssertUntilAsync(_ => eventHandler + .Received() + .Invoke( + Arg.Is( + payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderStale + ))); + } - var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")] + public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() + { + var eventHandler = Substitute.For(); - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); - // add the event handler after the provider has already transitioned into the ready state - myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(testProvider); - eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - } + await testProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); - [Fact] - [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] - [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] - [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] - [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] - [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")] - public async Task Client_Level_Event_Handlers_Should_Be_Removable() - { - var fixture = new Fixture(); + var newTestProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(newTestProvider); - var eventHandler = Substitute.For(); + await newTestProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); - var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + await Utils.AssertUntilAsync( + _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)) + ); + await Utils.AssertUntilAsync( + _ => eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); + } - myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + [Fact] + [Specification("5.2.2", "The `API` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")] + public async Task API_Level_Event_Handlers_Should_Be_Removable() + { + var eventHandler = Substitute.For(); - var testProvider = new TestProvider(); - await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - // wait for the first event to be received - await Utils.AssertUntilAsync( - _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) - ); + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(testProvider); - myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); + Thread.Sleep(1000); + Api.Instance.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); - // send another event from the provider - this one should not be received - await testProvider.SendEventAsync(ProviderEventTypes.ProviderReady); + var newTestProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(newTestProvider); - // wait a bit and make sure we only have received the first event, but nothing after removing the event handler - await Utils.AssertUntilAsync( - _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) - ); - } + eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } - [Fact] - public void RegisterClientFeatureProvider_WhenCalledWithNullProvider_DoesNotThrowException() - { - // Arrange - var eventExecutor = new EventExecutor(); - string client = "testClient"; - FeatureProvider? provider = null; - - // Act - var exception = Record.Exception(() => eventExecutor.RegisterClientFeatureProvider(client, provider)); - - // Assert - Assert.Null(exception); - } - - [Theory] - [InlineData(ProviderEventTypes.ProviderError, ProviderStatus.Error)] - [InlineData(ProviderEventTypes.ProviderReady, ProviderStatus.Ready)] - [InlineData(ProviderEventTypes.ProviderStale, ProviderStatus.Stale)] - [Specification("5.3.5", "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")] - public async Task Provider_Events_Should_Update_ProviderStatus(ProviderEventTypes type, ProviderStatus status) - { - var provider = new TestProvider(); - await Api.Instance.SetProviderAsync("5.3.5", provider); - _ = provider.SendEventAsync(type); - await Utils.AssertUntilAsync(_ => Assert.True(provider.Status == status)); - } + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] + public async Task API_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() + { + var fixture = new Fixture(); + + var failingEventHandler = Substitute.For(); + var eventHandler = Substitute.For(); + + failingEventHandler.When(x => x.Invoke(Arg.Any())) + .Do(x => throw new Exception()); + + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var testProvider = new TestProvider(fixture.Create()); + await Api.Instance.SetProviderAsync(testProvider); + + await Utils.AssertUntilAsync( + _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + await Utils.AssertUntilAsync( + _ => eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + public async Task Client_Level_Event_Handlers_Should_Be_Registered() + { + var fixture = new Fixture(); + var eventHandler = Substitute.For(); + + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var myClient = Api.Instance.GetClient(domain, clientVersion); + + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); + + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] + public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() + { + var fixture = new Fixture(); + + var failingEventHandler = Substitute.For(); + var eventHandler = Substitute.For(); + + failingEventHandler.When(x => x.Invoke(Arg.Any())) + .Do(x => throw new Exception()); + + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var myClient = Api.Instance.GetClient(domain, clientVersion); + + myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); + + await Utils.AssertUntilAsync( + _ => failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + await Utils.AssertUntilAsync( + _ => eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Provider() + { + var fixture = new Fixture(); + var eventHandler = Substitute.For(); + var clientEventHandler = Substitute.For(); + + var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + + var apiProvider = new TestProvider(fixture.Create()); + var clientProvider = new TestProvider(fixture.Create()); + + // set the default provider on API level, but not specifically to the client + await Api.Instance.SetProviderAsync(apiProvider); + // set the other provider specifically for the client + await Api.Instance.SetProviderAsync(myClientWithBoundProvider.GetMetadata().Name!, clientProvider); + + myClientWithNoBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + myClientWithBoundProvider.AddHandler(ProviderEventTypes.ProviderReady, clientEventHandler); + + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name)); + eventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name)); + + clientEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name)); + clientEventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")] + public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Named_Provider_Instead_of_Default() + { + var fixture = new Fixture(); + var clientEventHandler = Substitute.For(); + + var client = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + + var defaultProvider = new TestProvider(fixture.Create()); + var clientProvider = new TestProvider(fixture.Create()); + + // set the default provider + await Api.Instance.SetProviderAsync(defaultProvider); + + client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler); + + await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + + // verify that the client received the event from the default provider as there is no named provider registered yet + await Utils.AssertUntilAsync( + _ => clientEventHandler.Received(1) + .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); + + // set the other provider specifically for the client + await Api.Instance.SetProviderAsync(client.GetMetadata().Name!, clientProvider); + + // now, send another event for the default handler + await defaultProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + await clientProvider.SendEventAsync(ProviderEventTypes.ProviderConfigurationChanged); + + // now the client should have received only the event from the named provider + await Utils.AssertUntilAsync( + _ => clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); + // for the default provider, the number of received events should stay unchanged + await Utils.AssertUntilAsync( + _ => clientEventHandler.Received(1) + .Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)) + ); + } + + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.3.1", "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.")] + [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] + public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering() + { + var fixture = new Fixture(); + var eventHandler = Substitute.For(); + + var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); + + // add the event handler after the provider has already transitioned into the ready state + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + } + + [Fact] + [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.7", "The `API` and `client` MUST provide a function allowing the removal of event handlers.")] + public async Task Client_Level_Event_Handlers_Should_Be_Removable() + { + var fixture = new Fixture(); + + var eventHandler = Substitute.For(); + + var myClient = Api.Instance.GetClient(fixture.Create(), fixture.Create()); + + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + + var testProvider = new TestProvider(); + await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); + + // wait for the first event to be received + await Utils.AssertUntilAsync( + _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + + myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); + + // send another event from the provider - this one should not be received + await testProvider.SendEventAsync(ProviderEventTypes.ProviderReady); + + // wait a bit and make sure we only have received the first event, but nothing after removing the event handler + await Utils.AssertUntilAsync( + _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + } + + [Fact] + public void RegisterClientFeatureProvider_WhenCalledWithNullProvider_DoesNotThrowException() + { + // Arrange + var eventExecutor = new EventExecutor(); + string client = "testClient"; + FeatureProvider? provider = null; + + // Act + var exception = Record.Exception(() => eventExecutor.RegisterClientFeatureProvider(client, provider)); + + // Assert + Assert.Null(exception); + } + + [Theory] + [InlineData(ProviderEventTypes.ProviderError, ProviderStatus.Error)] + [InlineData(ProviderEventTypes.ProviderReady, ProviderStatus.Ready)] + [InlineData(ProviderEventTypes.ProviderStale, ProviderStatus.Stale)] + [Specification("5.3.5", "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")] + public async Task Provider_Events_Should_Update_ProviderStatus(ProviderEventTypes type, ProviderStatus status) + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync("5.3.5", provider); + _ = provider.SendEventAsync(type); + await Utils.AssertUntilAsync(_ => Assert.True(provider.Status == status)); } } diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index ae53f6db..d2e9b5e9 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -14,738 +14,737 @@ using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture + [Fact] + [Specification("1.5.1", "The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")] + [Specification("2.3.1", "The provider interface MUST define a `provider hook` mechanism which can be optionally implemented in order to add `hook` instances to the evaluation life-cycle.")] + [Specification("4.4.2", "Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API")] + public async Task Hooks_Should_Be_Called_In_Order() { - [Fact] - [Specification("1.5.1", "The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")] - [Specification("2.3.1", "The provider interface MUST define a `provider hook` mechanism which can be optionally implemented in order to add `hook` instances to the evaluation life-cycle.")] - [Specification("4.4.2", "Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API")] - public async Task Hooks_Should_Be_Called_In_Order() + var fixture = new Fixture(); + var domain = fixture.Create(); + var clientVersion = fixture.Create(); + var flagName = fixture.Create(); + var defaultValue = fixture.Create(); + var apiHook = Substitute.For(); + var clientHook = Substitute.For(); + var invocationHook = Substitute.For(); + var providerHook = Substitute.For(); + + // Sequence + apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); + + var testProvider = new TestProvider(); + testProvider.AddHook(providerHook); + Api.Instance.AddHooks(apiHook); + await Api.Instance.SetProviderAsync(testProvider); + var client = Api.Instance.GetClient(domain, clientVersion); + client.AddHooks(clientHook); + + await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empty, + new FlagEvaluationOptions(invocationHook, ImmutableDictionary.Empty)); + + Received.InOrder(() => { - var fixture = new Fixture(); - var domain = fixture.Create(); - var clientVersion = fixture.Create(); - var flagName = fixture.Create(); - var defaultValue = fixture.Create(); - var apiHook = Substitute.For(); - var clientHook = Substitute.For(); - var invocationHook = Substitute.For(); - var providerHook = Substitute.For(); - - // Sequence - apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()).Returns(new ValueTask()); - - var testProvider = new TestProvider(); - testProvider.AddHook(providerHook); - Api.Instance.AddHooks(apiHook); - await Api.Instance.SetProviderAsync(testProvider); - var client = Api.Instance.GetClient(domain, clientVersion); - client.AddHooks(clientHook); - - await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empty, - new FlagEvaluationOptions(invocationHook, ImmutableDictionary.Empty)); - - Received.InOrder(() => - { - apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()); - clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()); - invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()); - providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()); - providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - }); + apiHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + clientHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + invocationHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + providerHook.BeforeAsync(Arg.Any>(), Arg.Any>()); + providerHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + invocationHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + clientHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + apiHook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + providerHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + invocationHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + clientHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + apiHook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + }); + + _ = apiHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + } - _ = apiHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = apiHook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = providerHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = invocationHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = clientHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = apiHook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - } - - [Fact] - [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(() => - new HookContext(null, Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null), - new Metadata(null), EvaluationContext.Empty)); - - Assert.Throws(() => - new HookContext("test", Structure.Empty, FlagValueType.Object, null, - new Metadata(null), EvaluationContext.Empty)); - - Assert.Throws(() => - new HookContext("test", Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null), - null, EvaluationContext.Empty)); - - 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] - [Specification("4.1.2", "The `hook context` SHOULD provide: access to the `client metadata` and the `provider metadata` fields.")] - [Specification("4.1.3", "The `flag key`, `flag type`, and `default value` properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.")] - public void Hook_Context_Should_Have_Properties_And_Be_Immutable() - { - var clientMetadata = new ClientMetadata("client", "1.0.0"); - var providerMetadata = new Metadata("provider"); - var testStructure = Structure.Empty; - var context = new HookContext("test", testStructure, FlagValueType.Object, clientMetadata, - providerMetadata, EvaluationContext.Empty); - - Assert.Equal(clientMetadata, context.ClientMetadata); - Assert.Equal(providerMetadata, context.ProviderMetadata); - Assert.Equal("test", context.FlagKey); - Assert.Equal(testStructure, context.DefaultValue); - Assert.Equal(FlagValueType.Object, context.FlagValueType); - } - - [Fact] - [Specification("4.1.4", "The evaluation context MUST be mutable only within the `before` hook.")] - [Specification("4.3.3", "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")] - public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() - { - var evaluationContext = new EvaluationContextBuilder().Set("test", "test").Build(); - var hook1 = Substitute.For(); - var hook2 = Substitute.For(); - var hookContext = new HookContext("test", false, - FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName), - evaluationContext); - - hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(evaluationContext); - hook2.BeforeAsync(hookContext, Arg.Any>()).Returns(evaluationContext); - - 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).BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = 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 => + [Fact] + [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(() => + new HookContext(null, Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null), + new Metadata(null), EvaluationContext.Empty)); + + Assert.Throws(() => + new HookContext("test", Structure.Empty, FlagValueType.Object, null, + new Metadata(null), EvaluationContext.Empty)); + + Assert.Throws(() => + new HookContext("test", Structure.Empty, FlagValueType.Object, new ClientMetadata(null, null), + null, EvaluationContext.Empty)); + + 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] + [Specification("4.1.2", "The `hook context` SHOULD provide: access to the `client metadata` and the `provider metadata` fields.")] + [Specification("4.1.3", "The `flag key`, `flag type`, and `default value` properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.")] + public void Hook_Context_Should_Have_Properties_And_Be_Immutable() + { + var clientMetadata = new ClientMetadata("client", "1.0.0"); + var providerMetadata = new Metadata("provider"); + var testStructure = Structure.Empty; + var context = new HookContext("test", testStructure, FlagValueType.Object, clientMetadata, + providerMetadata, EvaluationContext.Empty); + + Assert.Equal(clientMetadata, context.ClientMetadata); + Assert.Equal(providerMetadata, context.ProviderMetadata); + Assert.Equal("test", context.FlagKey); + Assert.Equal(testStructure, context.DefaultValue); + Assert.Equal(FlagValueType.Object, context.FlagValueType); + } + + [Fact] + [Specification("4.1.4", "The evaluation context MUST be mutable only within the `before` hook.")] + [Specification("4.3.3", "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")] + public async Task Evaluation_Context_Must_Be_Mutable_Before_Hook() + { + var evaluationContext = new EvaluationContextBuilder().Set("test", "test").Build(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + var hookContext = new HookContext("test", false, + FlagValueType.Boolean, new ClientMetadata("test", "1.0.0"), new Metadata(NoOpProvider.NoOpProviderName), + evaluationContext); + + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(evaluationContext); + hook2.BeforeAsync(hookContext, Arg.Any>()).Returns(evaluationContext); + + 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).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = 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-b", "test-value"); + 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 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)); + 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>()); - } + _ = 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 => + [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-b", "test-value-hook-1"); + 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 => + hook2.BeforeAsync(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty).AndDoes(info => { - info.Arg>().Data.Set("hook-2-value-b", "test-value-hook-2"); + info.Arg>().Data.Set("hook-2-value-a", false); + info.Arg>().Data.Set("same", false); }); - - 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`.")] - public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() + hook2.AfterAsync(Arg.Any>(), Arg.Any>(), + Arg.Any>()).Returns(new ValueTask()).AndDoes(info => { - var propGlobal = "4.3.4global"; - var propGlobalToOverwrite = "4.3.4globalToOverwrite"; + 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>()); + } - var propClient = "4.3.4client"; - var propClientToOverwrite = "4.3.4clientToOverwrite"; + [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`.")] + public async Task Evaluation_Context_Must_Be_Merged_In_Correct_Order() + { + var propGlobal = "4.3.4global"; + var propGlobalToOverwrite = "4.3.4globalToOverwrite"; - var propInvocation = "4.3.4invocation"; - var propInvocationToOverwrite = "4.3.4invocationToOverwrite"; + var propClient = "4.3.4client"; + var propClientToOverwrite = "4.3.4clientToOverwrite"; - var propTransaction = "4.3.4transaction"; - var propTransactionToOverwrite = "4.3.4transactionToOverwrite"; + var propInvocation = "4.3.4invocation"; + var propInvocationToOverwrite = "4.3.4invocationToOverwrite"; - var propHook = "4.3.4hook"; + var propTransaction = "4.3.4transaction"; + var propTransactionToOverwrite = "4.3.4transactionToOverwrite"; - // setup a cascade of overwriting properties - Api.Instance.SetContext(new EvaluationContextBuilder() - .Set(propGlobal, true) - .Set(propGlobalToOverwrite, false) - .Build()); + var propHook = "4.3.4hook"; - var clientContext = new EvaluationContextBuilder() - .Set(propClient, true) - .Set(propGlobalToOverwrite, true) - .Set(propClientToOverwrite, false) - .Build(); + // setup a cascade of overwriting properties + Api.Instance.SetContext(new EvaluationContextBuilder() + .Set(propGlobal, true) + .Set(propGlobalToOverwrite, false) + .Build()); - var transactionContext = new EvaluationContextBuilder() - .Set(propTransaction, true) - .Set(propInvocationToOverwrite, true) - .Set(propTransactionToOverwrite, false) - .Build(); + var clientContext = new EvaluationContextBuilder() + .Set(propClient, true) + .Set(propGlobalToOverwrite, true) + .Set(propClientToOverwrite, false) + .Build(); - var invocationContext = new EvaluationContextBuilder() - .Set(propInvocation, true) - .Set(propClientToOverwrite, true) - .Set(propTransactionToOverwrite, true) - .Set(propInvocationToOverwrite, false) - .Build(); + var transactionContext = new EvaluationContextBuilder() + .Set(propTransaction, true) + .Set(propInvocationToOverwrite, true) + .Set(propTransactionToOverwrite, false) + .Build(); + var invocationContext = new EvaluationContextBuilder() + .Set(propInvocation, true) + .Set(propClientToOverwrite, true) + .Set(propTransactionToOverwrite, true) + .Set(propInvocationToOverwrite, false) + .Build(); - var hookContext = new EvaluationContextBuilder() - .Set(propHook, true) - .Set(propInvocationToOverwrite, true) - .Build(); - var transactionContextPropagator = new AsyncLocalTransactionContextPropagator(); - transactionContextPropagator.SetTransactionContext(transactionContext); - Api.Instance.SetTransactionContextPropagator(transactionContextPropagator); + var hookContext = new EvaluationContextBuilder() + .Set(propHook, true) + .Set(propInvocationToOverwrite, true) + .Build(); - var provider = Substitute.For(); + var transactionContextPropagator = new AsyncLocalTransactionContextPropagator(); + transactionContextPropagator.SetTransactionContext(transactionContext); + Api.Instance.SetTransactionContextPropagator(transactionContextPropagator); - provider.GetMetadata().Returns(new Metadata(null)); + var provider = Substitute.For(); - provider.GetProviderHooks().Returns(ImmutableList.Empty); + provider.GetMetadata().Returns(new Metadata(null)); - provider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); + provider.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(provider); + provider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", true)); - var hook = Substitute.For(); - hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(hookContext); + await Api.Instance.SetProviderAsync(provider); + var hook = Substitute.For(); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(hookContext); - var client = Api.Instance.GetClient("test", "1.0.0", null, clientContext); - await client.GetBooleanValueAsync("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty)); - // after proper merging, all properties should equal true - _ = provider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Is(y => - (y.GetValue(propGlobal).AsBoolean ?? false) - && (y.GetValue(propClient).AsBoolean ?? false) - && (y.GetValue(propTransaction).AsBoolean ?? false) - && (y.GetValue(propGlobalToOverwrite).AsBoolean ?? false) - && (y.GetValue(propTransactionToOverwrite).AsBoolean ?? false) - && (y.GetValue(propInvocation).AsBoolean ?? false) - && (y.GetValue(propClientToOverwrite).AsBoolean ?? false) - && (y.GetValue(propHook).AsBoolean ?? false) - && (y.GetValue(propInvocationToOverwrite).AsBoolean ?? false) - )); - } + var client = Api.Instance.GetClient("test", "1.0.0", null, clientContext); + await client.GetBooleanValueAsync("test", false, invocationContext, new FlagEvaluationOptions(ImmutableList.Create(hook), ImmutableDictionary.Empty)); - [Fact] - [Specification("4.2.1", "`hook hints` MUST be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`..")] - [Specification("4.2.2.1", "Condition: `Hook hints` MUST be immutable.")] - [Specification("4.2.2.2", "Condition: The client `metadata` field in the `hook context` MUST be immutable.")] - [Specification("4.2.2.3", "Condition: The provider `metadata` field in the `hook context` MUST be immutable.")] - [Specification("4.3.1", "Hooks MUST specify at least one stage.")] - public async Task Hook_Should_Return_No_Errors() - { - var hook = new TestHookNoOverride(); - var hookHints = new Dictionary - { - ["string"] = "test", - ["number"] = 1, - ["boolean"] = true, - ["datetime"] = DateTime.Now, - ["structure"] = Structure.Empty - }; - var hookContext = new HookContext("test", false, FlagValueType.Boolean, - new ClientMetadata(null, null), new Metadata(null), EvaluationContext.Empty); - var evaluationDetails = - new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"); - - await hook.BeforeAsync(hookContext, hookHints); - await hook.AfterAsync(hookContext, evaluationDetails, hookHints); - await hook.FinallyAsync(hookContext, evaluationDetails, hookHints); - await hook.ErrorAsync(hookContext, new Exception(), hookHints); - - Assert.Null(hookContext.ClientMetadata.Name); - Assert.Null(hookContext.ClientMetadata.Version); - Assert.Null(hookContext.ProviderMetadata.Name); - } - - [Fact] - [Specification("4.3.5", "The `after` stage MUST run after flag resolution occurs. It accepts a `hook context` (required), `flag evaluation details` (required) and `hook hints` (optional). It has no return value.")] - [Specification("4.3.6", "The `error` hook MUST run when errors are encountered in the `before` stage, the `after` stage or during flag resolution. It accepts `hook context` (required), `exception` representing what went wrong (required), and `hook hints` (optional). It has no return value.")] - [Specification("4.3.7", "The `finally` hook MUST run after the `before`, `after`, and `error` stages. It accepts a `hook context` (required) and `hook hints` (optional). There is no return value.")] - [Specification("4.5.1", "`Flag evaluation options` MAY contain `hook hints`, a map of data to be provided to hook invocations.")] - [Specification("4.5.2", "`hook hints` MUST be passed to each hook.")] - [Specification("4.5.3", "The hook MUST NOT alter the `hook hints` structure.")] - public async Task Hook_Should_Execute_In_Correct_Order() - { - var featureProvider = Substitute.For(); - var hook = Substitute.For(); + // after proper merging, all properties should equal true + _ = provider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Is(y => + (y.GetValue(propGlobal).AsBoolean ?? false) + && (y.GetValue(propClient).AsBoolean ?? false) + && (y.GetValue(propTransaction).AsBoolean ?? false) + && (y.GetValue(propGlobalToOverwrite).AsBoolean ?? false) + && (y.GetValue(propTransactionToOverwrite).AsBoolean ?? false) + && (y.GetValue(propInvocation).AsBoolean ?? false) + && (y.GetValue(propClientToOverwrite).AsBoolean ?? false) + && (y.GetValue(propHook).AsBoolean ?? false) + && (y.GetValue(propInvocationToOverwrite).AsBoolean ?? false) + )); + } - featureProvider.GetMetadata().Returns(new Metadata(null)); - featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + [Fact] + [Specification("4.2.1", "`hook hints` MUST be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`..")] + [Specification("4.2.2.1", "Condition: `Hook hints` MUST be immutable.")] + [Specification("4.2.2.2", "Condition: The client `metadata` field in the `hook context` MUST be immutable.")] + [Specification("4.2.2.3", "Condition: The provider `metadata` field in the `hook context` MUST be immutable.")] + [Specification("4.3.1", "Hooks MUST specify at least one stage.")] + public async Task Hook_Should_Return_No_Errors() + { + var hook = new TestHookNoOverride(); + var hookHints = new Dictionary + { + ["string"] = "test", + ["number"] = 1, + ["boolean"] = true, + ["datetime"] = DateTime.Now, + ["structure"] = Structure.Empty + }; + var hookContext = new HookContext("test", false, FlagValueType.Boolean, + new ClientMetadata(null, null), new Metadata(null), EvaluationContext.Empty); + var evaluationDetails = + new FlagEvaluationDetails("test", false, ErrorType.None, "testing", "testing"); + + await hook.BeforeAsync(hookContext, hookHints); + await hook.AfterAsync(hookContext, evaluationDetails, hookHints); + await hook.FinallyAsync(hookContext, evaluationDetails, hookHints); + await hook.ErrorAsync(hookContext, new Exception(), hookHints); + + Assert.Null(hookContext.ClientMetadata.Name); + Assert.Null(hookContext.ClientMetadata.Version); + Assert.Null(hookContext.ProviderMetadata.Name); + } - // Sequence - hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); - _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + [Fact] + [Specification("4.3.5", "The `after` stage MUST run after flag resolution occurs. It accepts a `hook context` (required), `flag evaluation details` (required) and `hook hints` (optional). It has no return value.")] + [Specification("4.3.6", "The `error` hook MUST run when errors are encountered in the `before` stage, the `after` stage or during flag resolution. It accepts `hook context` (required), `exception` representing what went wrong (required), and `hook hints` (optional). It has no return value.")] + [Specification("4.3.7", "The `finally` hook MUST run after the `before`, `after`, and `error` stages. It accepts a `hook context` (required) and `hook hints` (optional). There is no return value.")] + [Specification("4.5.1", "`Flag evaluation options` MAY contain `hook hints`, a map of data to be provided to hook invocations.")] + [Specification("4.5.2", "`hook hints` MUST be passed to each hook.")] + [Specification("4.5.3", "The hook MUST NOT alter the `hook hints` structure.")] + public async Task Hook_Should_Execute_In_Correct_Order() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); - await Api.Instance.SetProviderAsync(featureProvider); - var client = Api.Instance.GetClient(); - client.AddHooks(hook); + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - await client.GetBooleanValueAsync("test", false); + // Sequence + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - Received.InOrder(() => - { - hook.BeforeAsync(Arg.Any>(), Arg.Any>()); - featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); - hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - }); + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + client.AddHooks(hook); - _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } + await client.GetBooleanValueAsync("test", false); - [Fact] - [Specification("4.4.1", "The API, Client, Provider, and invocation MUST have a method for registering hooks.")] - public async Task Register_Hooks_Should_Be_Available_At_All_Levels() + Received.InOrder(() => { - var hook1 = Substitute.For(); - var hook2 = Substitute.For(); - var hook3 = Substitute.For(); - var hook4 = Substitute.For(); - - var testProvider = new TestProvider(); - testProvider.AddHook(hook4); - Api.Instance.AddHooks(hook1); - await Api.Instance.SetProviderAsync(testProvider); - var client = Api.Instance.GetClient(); - client.AddHooks(hook2); - await client.GetBooleanValueAsync("test", false, null, - new FlagEvaluationOptions(hook3, ImmutableDictionary.Empty)); - - Assert.Single(Api.Instance.GetHooks()); - Assert.Single(client.GetHooks()); - Assert.Single(testProvider.GetProviderHooks()); - } - - [Fact] - [Specification("4.4.3", "If a `finally` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `finally` hooks.")] - public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() - { - var featureProvider = Substitute.For(); - var hook1 = Substitute.For(); - var hook2 = Substitute.For(); - - featureProvider.GetMetadata().Returns(new Metadata(null)); - featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - - // Sequence - hook1.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); - hook2.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); - hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); - hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); - hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); - hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Throws(new Exception()); - - await Api.Instance.SetProviderAsync(featureProvider); - var client = Api.Instance.GetClient(); - client.AddHooks(new[] { hook1, hook2 }); - Assert.Equal(2, client.GetHooks().Count()); - - await client.GetBooleanValueAsync("test", false); - - Received.InOrder(() => - { - hook1.BeforeAsync(Arg.Any>(), null); - hook2.BeforeAsync(Arg.Any>(), null); - featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); - hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null); - hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null); - hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null); - hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null); - }); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + }); + + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } - _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); - _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); - _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); - _ = hook2.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); - _ = hook1.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); - _ = hook2.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); - _ = hook1.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); - } - - [Fact] - [Specification("4.4.4", "If an `error` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `error` hooks.")] - public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() - { - var featureProvider1 = Substitute.For(); - var hook1 = Substitute.For(); - var hook2 = Substitute.For(); + [Fact] + [Specification("4.4.1", "The API, Client, Provider, and invocation MUST have a method for registering hooks.")] + public async Task Register_Hooks_Should_Be_Available_At_All_Levels() + { + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + var hook3 = Substitute.For(); + var hook4 = Substitute.For(); + + var testProvider = new TestProvider(); + testProvider.AddHook(hook4); + Api.Instance.AddHooks(hook1); + await Api.Instance.SetProviderAsync(testProvider); + var client = Api.Instance.GetClient(); + client.AddHooks(hook2); + await client.GetBooleanValueAsync("test", false, null, + new FlagEvaluationOptions(hook3, ImmutableDictionary.Empty)); + + Assert.Single(Api.Instance.GetHooks()); + Assert.Single(client.GetHooks()); + Assert.Single(testProvider.GetProviderHooks()); + } - featureProvider1.GetMetadata().Returns(new Metadata(null)); - featureProvider1.GetProviderHooks().Returns(ImmutableList.Empty); + [Fact] + [Specification("4.4.3", "If a `finally` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `finally` hooks.")] + public async Task Finally_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() + { + var featureProvider = Substitute.For(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + + // Sequence + hook1.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + hook2.BeforeAsync(Arg.Any>(), null).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(new ResolutionDetails("test", false)); + hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Throws(new Exception()); + + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + client.AddHooks(new[] { hook1, hook2 }); + Assert.Equal(2, client.GetHooks().Count()); + + await client.GetBooleanValueAsync("test", false); + + Received.InOrder(() => + { + hook1.BeforeAsync(Arg.Any>(), null); + hook2.BeforeAsync(Arg.Any>(), null); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.AfterAsync(Arg.Any>(), Arg.Any>(), null); + hook1.AfterAsync(Arg.Any>(), Arg.Any>(), null); + hook2.FinallyAsync(Arg.Any>(), Arg.Any>(), null); + hook1.FinallyAsync(Arg.Any>(), Arg.Any>(), null); + }); + + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); + _ = featureProvider.Received(1).ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + _ = hook2.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook1.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook2.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); + _ = hook1.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); + } - // Sequence - 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).Throws(new Exception()); - hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); + [Fact] + [Specification("4.4.4", "If an `error` hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining `error` hooks.")] + public async Task Error_Hook_Should_Be_Executed_Even_If_Abnormal_Termination() + { + var featureProvider1 = Substitute.For(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); - await Api.Instance.SetProviderAsync(featureProvider1); - var client = Api.Instance.GetClient(); - client.AddHooks(new[] { hook1, hook2 }); + featureProvider1.GetMetadata().Returns(new Metadata(null)); + featureProvider1.GetProviderHooks().Returns(ImmutableList.Empty); - await client.GetBooleanValueAsync("test", false); + // Sequence + 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).Throws(new Exception()); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); - Received.InOrder(() => - { - hook1.BeforeAsync(Arg.Any>(), null); - hook2.BeforeAsync(Arg.Any>(), null); - featureProvider1.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); - hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); - hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); - }); + await Api.Instance.SetProviderAsync(featureProvider1); + var client = Api.Instance.GetClient(); + client.AddHooks(new[] { hook1, hook2 }); - _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); - _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); - _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); - _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); - } + await client.GetBooleanValueAsync("test", false); - [Fact] - [Specification("4.4.6", "If an error occurs during the evaluation of `before` or `after` hooks, any remaining hooks in the `before` or `after` stages MUST NOT be invoked.")] - public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_Any_Remaining_Hooks() + Received.InOrder(() => { - var featureProvider = Substitute.For(); - var hook1 = Substitute.For(); - var hook2 = Substitute.For(); - - featureProvider.GetMetadata().Returns(new Metadata(null)); - featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + hook1.BeforeAsync(Arg.Any>(), null); + hook2.BeforeAsync(Arg.Any>(), null); + featureProvider1.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); + }); + + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook2.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + } - // Sequence - hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(new Exception()); - _ = hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); - _ = hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); + [Fact] + [Specification("4.4.6", "If an error occurs during the evaluation of `before` or `after` hooks, any remaining hooks in the `before` or `after` stages MUST NOT be invoked.")] + public async Task Error_Occurs_During_Before_After_Evaluation_Should_Not_Invoke_Any_Remaining_Hooks() + { + var featureProvider = Substitute.For(); + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); - await Api.Instance.SetProviderAsync(featureProvider); - var client = Api.Instance.GetClient(); - client.AddHooks(new[] { hook1, hook2 }); + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - await client.GetBooleanValueAsync("test", false); + // Sequence + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(new Exception()); + _ = hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); - Received.InOrder(() => - { - hook1.BeforeAsync(Arg.Any>(), Arg.Any>()); - hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); - hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); - }); + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + client.AddHooks(new[] { hook1, hook2 }); - _ = hook1.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = hook2.DidNotReceive().BeforeAsync(Arg.Any>(), Arg.Any>()); - _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); - _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); - } + await client.GetBooleanValueAsync("test", false); - [Fact] - [Specification("4.5.1", "`Flag evaluation options` MAY contain `hook hints`, a map of data to be provided to hook invocations.")] - public async Task Hook_Hints_May_Be_Optional() + Received.InOrder(() => { - var featureProvider = Substitute.For(); - var hook = Substitute.For(); - var flagOptions = new FlagEvaluationOptions(hook); + hook1.BeforeAsync(Arg.Any>(), Arg.Any>()); + hook2.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook1.ErrorAsync(Arg.Any>(), Arg.Any(), null); + }); + + _ = hook1.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook2.DidNotReceive().BeforeAsync(Arg.Any>(), Arg.Any>()); + _ = hook1.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + _ = hook2.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), null); + } - featureProvider.GetMetadata() - .Returns(new Metadata(null)); + [Fact] + [Specification("4.5.1", "`Flag evaluation options` MAY contain `hook hints`, a map of data to be provided to hook invocations.")] + public async Task Hook_Hints_May_Be_Optional() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var flagOptions = new FlagEvaluationOptions(hook); - featureProvider.GetProviderHooks() - .Returns(ImmutableList.Empty); + featureProvider.GetMetadata() + .Returns(new Metadata(null)); - hook.BeforeAsync(Arg.Any>(), Arg.Any>()) - .Returns(EvaluationContext.Empty); + featureProvider.GetProviderHooks() + .Returns(ImmutableList.Empty); - featureProvider.ResolveBooleanValueAsync("test", false, Arg.Any()) - .Returns(new ResolutionDetails("test", false)); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty); - hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .Returns(new ValueTask()); + featureProvider.ResolveBooleanValueAsync("test", false, Arg.Any()) + .Returns(new ResolutionDetails("test", false)); - hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .Returns(new ValueTask()); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); - await Api.Instance.SetProviderAsync(featureProvider); - var client = Api.Instance.GetClient(); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); - await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, flagOptions); + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); - Received.InOrder(() => - { - hook.Received().BeforeAsync(Arg.Any>(), Arg.Any>()); - featureProvider.Received().ResolveBooleanValueAsync("test", false, Arg.Any()); - hook.Received().AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - }); - } + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, flagOptions); - [Fact] - [Specification("4.4.5", "If an error occurs in the `before` or `after` hooks, the `error` hooks MUST be invoked.")] - [Specification("4.4.7", "If an error occurs in the `before` hooks, the default value MUST be returned.")] - public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() + Received.InOrder(() => { - var featureProvider = Substitute.For(); - var hook = Substitute.For(); - var exceptionToThrow = new Exception("Fails during default"); - - featureProvider.GetMetadata().Returns(new Metadata(null)); + hook.Received().BeforeAsync(Arg.Any>(), Arg.Any>()); + featureProvider.Received().ResolveBooleanValueAsync("test", false, Arg.Any()); + hook.Received().AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received().FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + }); + } - // Sequence - hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(exceptionToThrow); - hook.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); - hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); + [Fact] + [Specification("4.4.5", "If an error occurs in the `before` or `after` hooks, the `error` hooks MUST be invoked.")] + [Specification("4.4.7", "If an error occurs in the `before` hooks, the default value MUST be returned.")] + public async Task When_Error_Occurs_In_Before_Hook_Should_Return_Default_Value() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var exceptionToThrow = new Exception("Fails during default"); - var client = Api.Instance.GetClient(); - client.AddHooks(hook); + featureProvider.GetMetadata().Returns(new Metadata(null)); - var resolvedFlag = await client.GetBooleanValueAsync("test", true); + // Sequence + hook.BeforeAsync(Arg.Any>(), Arg.Any>()).Throws(exceptionToThrow); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), null).Returns(new ValueTask()); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null).Returns(new ValueTask()); - Received.InOrder(() => - { - hook.BeforeAsync(Arg.Any>(), Arg.Any>()); - hook.ErrorAsync(Arg.Any>(), Arg.Any(), null); - hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null); - }); + var client = Api.Instance.GetClient(); + client.AddHooks(hook); - Assert.True(resolvedFlag); - _ = hook.Received(1).BeforeAsync(Arg.Any>(), null); - _ = hook.Received(1).ErrorAsync(Arg.Any>(), exceptionToThrow, null); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); - } + var resolvedFlag = await client.GetBooleanValueAsync("test", true); - [Fact] - [Specification("4.4.5", "If an error occurs in the `before` or `after` hooks, the `error` hooks MUST be invoked.")] - public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() + Received.InOrder(() => { - var featureProvider = Substitute.For(); - var hook = Substitute.For(); - var flagOptions = new FlagEvaluationOptions(hook); - var exceptionToThrow = new Exception("Fails during default"); - - featureProvider.GetMetadata() - .Returns(new Metadata(null)); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), null); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), null); + }); + + Assert.True(resolvedFlag); + _ = hook.Received(1).BeforeAsync(Arg.Any>(), null); + _ = hook.Received(1).ErrorAsync(Arg.Any>(), exceptionToThrow, null); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), null); + } - featureProvider.GetProviderHooks() - .Returns(ImmutableList.Empty); + [Fact] + [Specification("4.4.5", "If an error occurs in the `before` or `after` hooks, the `error` hooks MUST be invoked.")] + public async Task When_Error_Occurs_In_After_Hook_Should_Invoke_Error_Hook() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var flagOptions = new FlagEvaluationOptions(hook); + var exceptionToThrow = new Exception("Fails during default"); - hook.BeforeAsync(Arg.Any>(), Arg.Any>()) - .Returns(EvaluationContext.Empty); + featureProvider.GetMetadata() + .Returns(new Metadata(null)); - featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(new ResolutionDetails("test", false)); + featureProvider.GetProviderHooks() + .Returns(ImmutableList.Empty); - hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .Throws(exceptionToThrow); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty); - hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) - .Returns(new ValueTask()); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new ResolutionDetails("test", false)); - hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .Returns(new ValueTask()); + hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Throws(exceptionToThrow); - await Api.Instance.SetProviderAsync(featureProvider); - var client = Api.Instance.GetClient(); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) + .Returns(new ValueTask()); - var resolvedFlag = await client.GetBooleanValueAsync("test", true, config: flagOptions); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); - Assert.True(resolvedFlag); + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); - Received.InOrder(() => - { - hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); - hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); - }); + var resolvedFlag = await client.GetBooleanValueAsync("test", true, config: flagOptions); - await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); - } + Assert.True(resolvedFlag); - [Fact] - public async Task Successful_Resolution_Should_Pass_Cancellation_Token() + Received.InOrder(() => { - var featureProvider = Substitute.For(); - var hook = Substitute.For(); - var cts = new CancellationTokenSource(); + hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>()); + hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()); + }); - featureProvider.GetMetadata().Returns(new Metadata(null)); - featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); + await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); + } - hook.BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token).Returns(EvaluationContext.Empty); - featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any(), cts.Token).Returns(new ResolutionDetails("test", false)); - _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); - _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + [Fact] + public async Task Successful_Resolution_Should_Pass_Cancellation_Token() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var cts = new CancellationTokenSource(); - await Api.Instance.SetProviderAsync(featureProvider); - var client = Api.Instance.GetClient(); - client.AddHooks(hook); + featureProvider.GetMetadata().Returns(new Metadata(null)); + featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); - await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, null, cts.Token); + hook.BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token).Returns(EvaluationContext.Empty); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any(), cts.Token).Returns(new ResolutionDetails("test", false)); + _ = hook.AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); - _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); - _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); - } + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); + client.AddHooks(hook); - [Fact] - public async Task Failed_Resolution_Should_Pass_Cancellation_Token() - { - var featureProvider = Substitute.For(); - var hook = Substitute.For(); - var flagOptions = new FlagEvaluationOptions(hook); - var exceptionToThrow = new GeneralException("Fake Exception"); - var cts = new CancellationTokenSource(); + await client.GetBooleanValueAsync("test", false, EvaluationContext.Empty, null, cts.Token); + + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).AfterAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + } - featureProvider.GetMetadata() - .Returns(new Metadata(null)); + [Fact] + public async Task Failed_Resolution_Should_Pass_Cancellation_Token() + { + var featureProvider = Substitute.For(); + var hook = Substitute.For(); + var flagOptions = new FlagEvaluationOptions(hook); + var exceptionToThrow = new GeneralException("Fake Exception"); + var cts = new CancellationTokenSource(); - featureProvider.GetProviderHooks() - .Returns(ImmutableList.Empty); + featureProvider.GetMetadata() + .Returns(new Metadata(null)); - hook.BeforeAsync(Arg.Any>(), Arg.Any>()) - .Returns(EvaluationContext.Empty); + featureProvider.GetProviderHooks() + .Returns(ImmutableList.Empty); - featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Throws(exceptionToThrow); + hook.BeforeAsync(Arg.Any>(), Arg.Any>()) + .Returns(EvaluationContext.Empty); - hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) - .Returns(new ValueTask()); + featureProvider.ResolveBooleanValueAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Throws(exceptionToThrow); - hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) - .Returns(new ValueTask()); + hook.ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>()) + .Returns(new ValueTask()); - await Api.Instance.SetProviderAsync(featureProvider); - var client = Api.Instance.GetClient(); + hook.FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(new ValueTask()); - await client.GetBooleanValueAsync("test", true, EvaluationContext.Empty, flagOptions, cts.Token); + await Api.Instance.SetProviderAsync(featureProvider); + var client = Api.Instance.GetClient(); - _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); - _ = hook.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>(), cts.Token); - _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); + await client.GetBooleanValueAsync("test", true, EvaluationContext.Empty, flagOptions, cts.Token); - await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); - } + _ = hook.Received(1).BeforeAsync(Arg.Any>(), Arg.Any>(), cts.Token); + _ = hook.Received(1).ErrorAsync(Arg.Any>(), Arg.Any(), Arg.Any>(), cts.Token); + _ = hook.Received(1).FinallyAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), cts.Token); - [Fact] - public void Add_hooks_should_accept_empty_enumerable() - { - Api.Instance.ClearHooks(); - Api.Instance.AddHooks(Enumerable.Empty()); - } + await featureProvider.DidNotReceive().ResolveBooleanValueAsync("test", false, Arg.Any()); + } + + [Fact] + public void Add_hooks_should_accept_empty_enumerable() + { + Api.Instance.ClearHooks(); + Api.Instance.AddHooks(Enumerable.Empty()); } } diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 24caf9ad..4dea7f39 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -8,313 +8,312 @@ using OpenFeature.Tests.Internal; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class OpenFeatureTests : ClearOpenFeatureInstanceFixture + [Fact] + [Specification("1.1.1", "The `API`, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.")] + public void OpenFeature_Should_Be_Singleton() + { + var openFeature = Api.Instance; + var openFeature2 = Api.Instance; + + Assert.Equal(openFeature2, openFeature); + } + + [Fact] + [Specification("1.1.2.2", "The provider mutator function MUST invoke the initialize function on the newly registered provider before using it to resolve flag values.")] + public async Task OpenFeature_Should_Initialize_Provider() + { + var providerMockDefault = Substitute.For(); + providerMockDefault.Status.Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProviderAsync(providerMockDefault); + await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()); + + var providerMockNamed = Substitute.For(); + providerMockNamed.Status.Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProviderAsync("the-name", providerMockNamed); + await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()); + } + + [Fact] + [Specification("1.1.2.3", + "The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no longer being used to resolve flag values.")] + public async Task OpenFeature_Should_Shutdown_Unused_Provider() + { + var providerA = Substitute.For(); + providerA.Status.Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProviderAsync(providerA); + await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()); + + var providerB = Substitute.For(); + providerB.Status.Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProviderAsync(providerB); + await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()); + await providerA.Received(1).ShutdownAsync(); + + var providerC = Substitute.For(); + providerC.Status.Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProviderAsync("named", providerC); + await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()); + + var providerD = Substitute.For(); + providerD.Status.Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProviderAsync("named", providerD); + await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()); + await providerC.Received(1).ShutdownAsync(); + } + + [Fact] + [Specification("1.6.1", "The API MUST define a mechanism to propagate a shutdown request to active providers.")] + public async Task OpenFeature_Should_Support_Shutdown() + { + var providerA = Substitute.For(); + providerA.Status.Returns(ProviderStatus.NotReady); + + var providerB = Substitute.For(); + providerB.Status.Returns(ProviderStatus.NotReady); + + await Api.Instance.SetProviderAsync(providerA); + await Api.Instance.SetProviderAsync("named", providerB); + + await Api.Instance.ShutdownAsync(); + + await providerA.Received(1).ShutdownAsync(); + await providerB.Received(1).ShutdownAsync(); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider() + { + var openFeature = Api.Instance; + + await openFeature.SetProviderAsync(new NoOpFeatureProvider()); + await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()); + + var defaultClient = openFeature.GetProviderMetadata(); + var domainScopedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); + + Assert.Equal(NoOpProvider.NoOpProviderName, defaultClient?.Name); + Assert.Equal(TestProvider.DefaultName, domainScopedClient?.Name); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() + { + var openFeature = Api.Instance; + + await openFeature.SetProviderAsync(new TestProvider()); + + var defaultClient = openFeature.GetProviderMetadata(); + + Assert.Equal(TestProvider.DefaultName, defaultClient?.Name); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() + { + const string name = "new-client"; + var openFeature = Api.Instance; + + await openFeature.SetProviderAsync(name, new TestProvider()); + await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); + + Assert.Equal(NoOpProvider.NoOpProviderName, openFeature.GetProviderMetadata(name)?.Name); + } + + [Fact] + [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] + public async Task OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance() + { + var openFeature = Api.Instance; + var provider = new TestProvider(); + + await openFeature.SetProviderAsync("a", provider); + await openFeature.SetProviderAsync("b", provider); + + var clientA = openFeature.GetProvider("a"); + var clientB = openFeature.GetProvider("b"); + + Assert.Equal(clientB, clientA); + } + + [Fact] + [Specification("1.1.4", "The `API` MUST provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] + public void OpenFeature_Should_Add_Hooks() + { + var openFeature = Api.Instance; + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + var hook3 = Substitute.For(); + var hook4 = Substitute.For(); + + openFeature.ClearHooks(); + + openFeature.AddHooks(hook1); + + Assert.Contains(hook1, openFeature.GetHooks()); + Assert.Single(openFeature.GetHooks()); + + openFeature.AddHooks(hook2); + var expectedHooks = new[] { hook1, hook2 }.AsEnumerable(); + Assert.Equal(expectedHooks, openFeature.GetHooks()); + + openFeature.AddHooks(new[] { hook3, hook4 }); + expectedHooks = new[] { hook1, hook2, hook3, hook4 }.AsEnumerable(); + Assert.Equal(expectedHooks, openFeature.GetHooks()); + + openFeature.ClearHooks(); + Assert.Empty(openFeature.GetHooks()); + } + + [Fact] + [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] + public async Task OpenFeature_Should_Get_Metadata() + { + await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); + var openFeature = Api.Instance; + var metadata = openFeature.GetProviderMetadata(); + + Assert.NotNull(metadata); + Assert.Equal(NoOpProvider.NoOpProviderName, metadata?.Name); + } + + [Theory] + [InlineData("client1", "version1")] + [InlineData("client2", null)] + [InlineData(null, null)] + [Specification("1.1.6", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] + public void OpenFeature_Should_Create_Client(string? name = null, string? version = null) { - [Fact] - [Specification("1.1.1", "The `API`, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.")] - public void OpenFeature_Should_Be_Singleton() - { - var openFeature = Api.Instance; - var openFeature2 = Api.Instance; + var openFeature = Api.Instance; + var client = openFeature.GetClient(name, version); - Assert.Equal(openFeature2, openFeature); - } + Assert.NotNull(client); + Assert.Equal(name, client.GetMetadata().Name); + Assert.Equal(version, client.GetMetadata().Version); + } - [Fact] - [Specification("1.1.2.2", "The provider mutator function MUST invoke the initialize function on the newly registered provider before using it to resolve flag values.")] - public async Task OpenFeature_Should_Initialize_Provider() - { - var providerMockDefault = Substitute.For(); - providerMockDefault.Status.Returns(ProviderStatus.NotReady); + [Fact] + public void Should_Set_Given_Context() + { + var context = EvaluationContext.Empty; - await Api.Instance.SetProviderAsync(providerMockDefault); - await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()); + Api.Instance.SetContext(context); - var providerMockNamed = Substitute.For(); - providerMockNamed.Status.Returns(ProviderStatus.NotReady); + Assert.Equal(context, Api.Instance.GetContext()); - await Api.Instance.SetProviderAsync("the-name", providerMockNamed); - await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()); - } + context = EvaluationContext.Builder().Build(); - [Fact] - [Specification("1.1.2.3", - "The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no longer being used to resolve flag values.")] - public async Task OpenFeature_Should_Shutdown_Unused_Provider() - { - var providerA = Substitute.For(); - providerA.Status.Returns(ProviderStatus.NotReady); + Api.Instance.SetContext(context); - await Api.Instance.SetProviderAsync(providerA); - await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()); + Assert.Equal(context, Api.Instance.GetContext()); + } - var providerB = Substitute.For(); - providerB.Status.Returns(ProviderStatus.NotReady); - - await Api.Instance.SetProviderAsync(providerB); - await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()); - await providerA.Received(1).ShutdownAsync(); - - var providerC = Substitute.For(); - providerC.Status.Returns(ProviderStatus.NotReady); - - await Api.Instance.SetProviderAsync("named", providerC); - await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()); + [Fact] + public void Should_Always_Have_Provider() + { + Assert.NotNull(Api.Instance.GetProvider()); + } - var providerD = Substitute.For(); - providerD.Status.Returns(ProviderStatus.NotReady); + [Fact] + public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() + { + var openFeature = Api.Instance; - await Api.Instance.SetProviderAsync("named", providerD); - await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()); - await providerC.Received(1).ShutdownAsync(); - } + await openFeature.SetProviderAsync("client1", new TestProvider()); + await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()); - [Fact] - [Specification("1.6.1", "The API MUST define a mechanism to propagate a shutdown request to active providers.")] - public async Task OpenFeature_Should_Support_Shutdown() - { - var providerA = Substitute.For(); - providerA.Status.Returns(ProviderStatus.NotReady); + var client1 = openFeature.GetClient("client1"); + var client2 = openFeature.GetClient("client2"); - var providerB = Substitute.For(); - providerB.Status.Returns(ProviderStatus.NotReady); - - await Api.Instance.SetProviderAsync(providerA); - await Api.Instance.SetProviderAsync("named", providerB); - - await Api.Instance.ShutdownAsync(); - - await providerA.Received(1).ShutdownAsync(); - await providerB.Received(1).ShutdownAsync(); - } - - [Fact] - [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Default_Provider() - { - var openFeature = Api.Instance; - - await openFeature.SetProviderAsync(new NoOpFeatureProvider()); - await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()); - - var defaultClient = openFeature.GetProviderMetadata(); - var domainScopedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); - - Assert.Equal(NoOpProvider.NoOpProviderName, defaultClient?.Name); - Assert.Equal(TestProvider.DefaultName, domainScopedClient?.Name); - } - - [Fact] - [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() - { - var openFeature = Api.Instance; - - await openFeature.SetProviderAsync(new TestProvider()); - - var defaultClient = openFeature.GetProviderMetadata(); - - Assert.Equal(TestProvider.DefaultName, defaultClient?.Name); - } + Assert.Equal("client1", client1.GetMetadata().Name); + Assert.Equal("client2", client2.GetMetadata().Name); - [Fact] - [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() - { - const string name = "new-client"; - var openFeature = Api.Instance; + Assert.True(await client1.GetBooleanValueAsync("test", false)); + Assert.False(await client2.GetBooleanValueAsync("test", false)); + } - await openFeature.SetProviderAsync(name, new TestProvider()); - await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); + [Fact] + public void SetTransactionContextPropagator_ShouldThrowArgumentNullException_WhenNullPropagatorIsPassed() + { + // Arrange + var api = Api.Instance; - Assert.Equal(NoOpProvider.NoOpProviderName, openFeature.GetProviderMetadata(name)?.Name); - } + // Act & Assert + Assert.Throws(() => api.SetTransactionContextPropagator(null!)); + } - [Fact] - [Specification("1.1.3", "The `API` MUST provide a function to bind a given `provider` to one or more client `name`s. If the client-name already has a bound provider, it is overwritten with the new mapping.")] - public async Task OpenFeature_Should_Allow_Multiple_Client_Names_Of_Same_Instance() - { - var openFeature = Api.Instance; - var provider = new TestProvider(); + [Fact] + public void SetTransactionContextPropagator_ShouldSetPropagator_WhenValidPropagatorIsPassed() + { + // Arrange + var api = Api.Instance; + var mockPropagator = Substitute.For(); - await openFeature.SetProviderAsync("a", provider); - await openFeature.SetProviderAsync("b", provider); + // Act + api.SetTransactionContextPropagator(mockPropagator); - var clientA = openFeature.GetProvider("a"); - var clientB = openFeature.GetProvider("b"); + // Assert + Assert.Equal(mockPropagator, api.GetTransactionContextPropagator()); + } + + [Fact] + public void SetTransactionContext_ShouldThrowArgumentNullException_WhenEvaluationContextIsNull() + { + // Arrange + var api = Api.Instance; + + // Act & Assert + Assert.Throws(() => api.SetTransactionContext(null!)); + } + + [Fact] + public void SetTransactionContext_ShouldSetTransactionContext_WhenValidEvaluationContextIsProvided() + { + // Arrange + var api = Api.Instance; + var evaluationContext = EvaluationContext.Builder() + .Set("initial", "yes") + .Build(); + var mockPropagator = Substitute.For(); + mockPropagator.GetTransactionContext().Returns(evaluationContext); + api.SetTransactionContextPropagator(mockPropagator); + api.SetTransactionContext(evaluationContext); + + // Act + api.SetTransactionContext(evaluationContext); + var result = api.GetTransactionContext(); + + // Assert + mockPropagator.Received().SetTransactionContext(evaluationContext); + Assert.Equal(evaluationContext, result); + Assert.Equal(evaluationContext.GetValue("initial"), result.GetValue("initial")); + } + + [Fact] + public void GetTransactionContext_ShouldReturnEmptyEvaluationContext_WhenNoPropagatorIsSet() + { + // Arrange + var api = Api.Instance; + var context = EvaluationContext.Builder().Set("status", "not-ready").Build(); + api.SetTransactionContext(context); - Assert.Equal(clientB, clientA); - } + // Act + var result = api.GetTransactionContext(); - [Fact] - [Specification("1.1.4", "The `API` MUST provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")] - public void OpenFeature_Should_Add_Hooks() - { - var openFeature = Api.Instance; - var hook1 = Substitute.For(); - var hook2 = Substitute.For(); - var hook3 = Substitute.For(); - var hook4 = Substitute.For(); - - openFeature.ClearHooks(); - - openFeature.AddHooks(hook1); - - Assert.Contains(hook1, openFeature.GetHooks()); - Assert.Single(openFeature.GetHooks()); - - openFeature.AddHooks(hook2); - var expectedHooks = new[] { hook1, hook2 }.AsEnumerable(); - Assert.Equal(expectedHooks, openFeature.GetHooks()); - - openFeature.AddHooks(new[] { hook3, hook4 }); - expectedHooks = new[] { hook1, hook2, hook3, hook4 }.AsEnumerable(); - Assert.Equal(expectedHooks, openFeature.GetHooks()); - - openFeature.ClearHooks(); - Assert.Empty(openFeature.GetHooks()); - } - - [Fact] - [Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")] - public async Task OpenFeature_Should_Get_Metadata() - { - await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); - var openFeature = Api.Instance; - var metadata = openFeature.GetProviderMetadata(); - - Assert.NotNull(metadata); - Assert.Equal(NoOpProvider.NoOpProviderName, metadata?.Name); - } - - [Theory] - [InlineData("client1", "version1")] - [InlineData("client2", null)] - [InlineData(null, null)] - [Specification("1.1.6", "The `API` MUST provide a function for creating a `client` which accepts the following options: - name (optional): A logical string identifier for the client.")] - public void OpenFeature_Should_Create_Client(string? name = null, string? version = null) - { - var openFeature = Api.Instance; - var client = openFeature.GetClient(name, version); - - Assert.NotNull(client); - Assert.Equal(name, client.GetMetadata().Name); - Assert.Equal(version, client.GetMetadata().Version); - } - - [Fact] - public void Should_Set_Given_Context() - { - var context = EvaluationContext.Empty; - - Api.Instance.SetContext(context); - - Assert.Equal(context, Api.Instance.GetContext()); - - context = EvaluationContext.Builder().Build(); - - Api.Instance.SetContext(context); - - Assert.Equal(context, Api.Instance.GetContext()); - } - - [Fact] - public void Should_Always_Have_Provider() - { - Assert.NotNull(Api.Instance.GetProvider()); - } - - [Fact] - public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping() - { - var openFeature = Api.Instance; - - await openFeature.SetProviderAsync("client1", new TestProvider()); - await openFeature.SetProviderAsync("client2", new NoOpFeatureProvider()); - - var client1 = openFeature.GetClient("client1"); - var client2 = openFeature.GetClient("client2"); - - Assert.Equal("client1", client1.GetMetadata().Name); - Assert.Equal("client2", client2.GetMetadata().Name); - - Assert.True(await client1.GetBooleanValueAsync("test", false)); - Assert.False(await client2.GetBooleanValueAsync("test", false)); - } - - [Fact] - public void SetTransactionContextPropagator_ShouldThrowArgumentNullException_WhenNullPropagatorIsPassed() - { - // Arrange - var api = Api.Instance; - - // Act & Assert - Assert.Throws(() => api.SetTransactionContextPropagator(null!)); - } - - [Fact] - public void SetTransactionContextPropagator_ShouldSetPropagator_WhenValidPropagatorIsPassed() - { - // Arrange - var api = Api.Instance; - var mockPropagator = Substitute.For(); - - // Act - api.SetTransactionContextPropagator(mockPropagator); - - // Assert - Assert.Equal(mockPropagator, api.GetTransactionContextPropagator()); - } - - [Fact] - public void SetTransactionContext_ShouldThrowArgumentNullException_WhenEvaluationContextIsNull() - { - // Arrange - var api = Api.Instance; - - // Act & Assert - Assert.Throws(() => api.SetTransactionContext(null!)); - } - - [Fact] - public void SetTransactionContext_ShouldSetTransactionContext_WhenValidEvaluationContextIsProvided() - { - // Arrange - var api = Api.Instance; - var evaluationContext = EvaluationContext.Builder() - .Set("initial", "yes") - .Build(); - var mockPropagator = Substitute.For(); - mockPropagator.GetTransactionContext().Returns(evaluationContext); - api.SetTransactionContextPropagator(mockPropagator); - api.SetTransactionContext(evaluationContext); - - // Act - api.SetTransactionContext(evaluationContext); - var result = api.GetTransactionContext(); - - // Assert - mockPropagator.Received().SetTransactionContext(evaluationContext); - Assert.Equal(evaluationContext, result); - Assert.Equal(evaluationContext.GetValue("initial"), result.GetValue("initial")); - } - - [Fact] - public void GetTransactionContext_ShouldReturnEmptyEvaluationContext_WhenNoPropagatorIsSet() - { - // Arrange - var api = Api.Instance; - var context = EvaluationContext.Builder().Set("status", "not-ready").Build(); - api.SetTransactionContext(context); - - // Act - var result = api.GetTransactionContext(); - - // Assert - Assert.Equal(EvaluationContext.Empty, result); - } + // Assert + Assert.Equal(EvaluationContext.Empty, result); } } diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index e88de6e9..046d750a 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -9,407 +9,406 @@ // We intentionally do not await for purposes of validating behavior. #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class ProviderRepositoryTests { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class ProviderRepositoryTests + [Fact] + public async Task Default_Provider_Is_Set_Without_Await() { - [Fact] - public async Task Default_Provider_Is_Set_Without_Await() - { - var repository = new ProviderRepository(); - var provider = new NoOpFeatureProvider(); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider, context); - Assert.Equal(provider, repository.GetProvider()); - } - - [Fact] - public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Default_Provider() - { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(providerMock, context); - providerMock.Received(1).InitializeAsync(context); - providerMock.DidNotReceive().ShutdownAsync(); - } - - [Fact] - public async Task AfterInitialization_Is_Invoked_For_Setting_Default_Provider() - { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - await repository.SetProviderAsync(providerMock, context, afterInitSuccess: (theProvider) => - { - Assert.Equal(providerMock, theProvider); - callCount++; - return Task.CompletedTask; - }); - Assert.Equal(1, callCount); - } + var repository = new ProviderRepository(); + var provider = new NoOpFeatureProvider(); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync(provider, context); + Assert.Equal(provider, repository.GetProvider()); + } - [Fact] - public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provider() - { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); - var callCount = 0; - Exception? receivedError = null; - await repository.SetProviderAsync(providerMock, context, afterInitError: (theProvider, error) => - { - Assert.Equal(providerMock, theProvider); - callCount++; - receivedError = error; - return Task.CompletedTask; - }); - Assert.Equal("BAD THINGS", receivedError?.Message); - Assert.Equal(1, callCount); - } - - [Theory] - [InlineData(ProviderStatus.Ready)] - [InlineData(ProviderStatus.Stale)] - [InlineData(ProviderStatus.Error)] - internal async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) - { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(status); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(providerMock, context); - providerMock.DidNotReceive().InitializeAsync(context); - } - - [Theory] - [InlineData(ProviderStatus.Ready)] - [InlineData(ProviderStatus.Stale)] - [InlineData(ProviderStatus.Error)] - internal async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) - { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(status); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - await repository.SetProviderAsync(providerMock, context, afterInitSuccess: provider => - { - callCount++; - return Task.CompletedTask; - }); - Assert.Equal(0, callCount); - } + [Fact] + public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Default_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync(providerMock, context); + providerMock.Received(1).InitializeAsync(context); + providerMock.DidNotReceive().ShutdownAsync(); + } - [Fact] - public async Task Replaced_Default_Provider_Is_Shutdown() + [Fact] + public async Task AfterInitialization_Is_Invoked_For_Setting_Default_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProviderAsync(providerMock, context, afterInitSuccess: (theProvider) => { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider1, context); - await repository.SetProviderAsync(provider2, context); - provider1.Received(1).ShutdownAsync(); - provider2.DidNotReceive().ShutdownAsync(); - } - - [Fact] - public async Task Named_Provider_Provider_Is_Set_Without_Await() + Assert.Equal(providerMock, theProvider); + callCount++; + return Task.CompletedTask; + }); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); + var callCount = 0; + Exception? receivedError = null; + await repository.SetProviderAsync(providerMock, context, afterInitError: (theProvider, error) => { - var repository = new ProviderRepository(); - var provider = new NoOpFeatureProvider(); - var context = new EvaluationContextBuilder().Build(); + Assert.Equal(providerMock, theProvider); + callCount++; + receivedError = error; + return Task.CompletedTask; + }); + Assert.Equal("BAD THINGS", receivedError?.Message); + Assert.Equal(1, callCount); + } - await repository.SetProviderAsync("the-name", provider, context); - Assert.Equal(provider, repository.GetProvider("the-name")); - } + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + internal async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(status); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync(providerMock, context); + providerMock.DidNotReceive().InitializeAsync(context); + } - [Fact] - public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Named_Provider() + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + internal async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(status); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProviderAsync(providerMock, context, afterInitSuccess: provider => { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync("the-name", providerMock, context); - providerMock.Received(1).InitializeAsync(context); - providerMock.DidNotReceive().ShutdownAsync(); - } - - [Fact] - public async Task AfterInitialization_Is_Invoked_For_Setting_Named_Provider() + callCount++; + return Task.CompletedTask; + }); + Assert.Equal(0, callCount); + } + + [Fact] + public async Task Replaced_Default_Provider_Is_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync(provider2, context); + provider1.Received(1).ShutdownAsync(); + provider2.DidNotReceive().ShutdownAsync(); + } + + [Fact] + public async Task Named_Provider_Provider_Is_Set_Without_Await() + { + var repository = new ProviderRepository(); + var provider = new NoOpFeatureProvider(); + var context = new EvaluationContextBuilder().Build(); + + await repository.SetProviderAsync("the-name", provider, context); + Assert.Equal(provider, repository.GetProvider("the-name")); + } + + [Fact] + public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Named_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync("the-name", providerMock, context); + providerMock.Received(1).InitializeAsync(context); + providerMock.DidNotReceive().ShutdownAsync(); + } + + [Fact] + public async Task AfterInitialization_Is_Invoked_For_Setting_Named_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProviderAsync("the-name", providerMock, context, afterInitSuccess: (theProvider) => { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - await repository.SetProviderAsync("the-name", providerMock, context, afterInitSuccess: (theProvider) => - { - Assert.Equal(providerMock, theProvider); - callCount++; - return Task.CompletedTask; - }); - Assert.Equal(1, callCount); - } + Assert.Equal(providerMock, theProvider); + callCount++; + return Task.CompletedTask; + }); + Assert.Equal(1, callCount); + } - [Fact] - public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider() + [Fact] + public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider() + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); + var callCount = 0; + Exception? receivedError = null; + await repository.SetProviderAsync("the-provider", providerMock, context, afterInitError: (theProvider, error) => { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); - var callCount = 0; - Exception? receivedError = null; - await repository.SetProviderAsync("the-provider", providerMock, context, afterInitError: (theProvider, error) => + Assert.Equal(providerMock, theProvider); + callCount++; + receivedError = error; + return Task.CompletedTask; + }); + Assert.Equal("BAD THINGS", receivedError?.Message); + Assert.Equal(1, callCount); + } + + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + internal async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(status); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync("the-name", providerMock, context); + providerMock.DidNotReceive().InitializeAsync(context); + } + + [Theory] + [InlineData(ProviderStatus.Ready)] + [InlineData(ProviderStatus.Stale)] + [InlineData(ProviderStatus.Error)] + internal async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + { + var repository = new ProviderRepository(); + var providerMock = Substitute.For(); + providerMock.Status.Returns(status); + var context = new EvaluationContextBuilder().Build(); + var callCount = 0; + await repository.SetProviderAsync("the-name", providerMock, context, + afterInitSuccess: provider => { - Assert.Equal(providerMock, theProvider); callCount++; - receivedError = error; return Task.CompletedTask; }); - Assert.Equal("BAD THINGS", receivedError?.Message); - Assert.Equal(1, callCount); - } - - [Theory] - [InlineData(ProviderStatus.Ready)] - [InlineData(ProviderStatus.Stale)] - [InlineData(ProviderStatus.Error)] - internal async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) - { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(status); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync("the-name", providerMock, context); - providerMock.DidNotReceive().InitializeAsync(context); - } - - [Theory] - [InlineData(ProviderStatus.Ready)] - [InlineData(ProviderStatus.Stale)] - [InlineData(ProviderStatus.Error)] - internal async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) - { - var repository = new ProviderRepository(); - var providerMock = Substitute.For(); - providerMock.Status.Returns(status); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - await repository.SetProviderAsync("the-name", providerMock, context, - afterInitSuccess: provider => - { - callCount++; - return Task.CompletedTask; - }); - Assert.Equal(0, callCount); - } - - [Fact] - public async Task Replaced_Named_Provider_Is_Shutdown() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync("the-name", provider1, context); - await repository.SetProviderAsync("the-name", provider2, context); - provider1.Received(1).ShutdownAsync(); - provider2.DidNotReceive().ShutdownAsync(); - } - - [Fact] - public async Task In_Use_Provider_Named_And_Default_Is_Not_Shutdown() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); + Assert.Equal(0, callCount); + } - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); + [Fact] + public async Task Replaced_Named_Provider_Is_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); + + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); + + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync("the-name", provider1, context); + await repository.SetProviderAsync("the-name", provider2, context); + provider1.Received(1).ShutdownAsync(); + provider2.DidNotReceive().ShutdownAsync(); + } - var context = new EvaluationContextBuilder().Build(); + [Fact] + public async Task In_Use_Provider_Named_And_Default_Is_Not_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync(provider1, context); - await repository.SetProviderAsync("A", provider1, context); - // Provider one is replaced for "A", but not default. - await repository.SetProviderAsync("A", provider2, context); + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); - provider1.DidNotReceive().ShutdownAsync(); - } + var context = new EvaluationContextBuilder().Build(); - [Fact] - public async Task In_Use_Provider_Two_Named_Is_Not_Shutdown() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("A", provider1, context); + // Provider one is replaced for "A", but not default. + await repository.SetProviderAsync("A", provider2, context); - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); + provider1.DidNotReceive().ShutdownAsync(); + } - var context = new EvaluationContextBuilder().Build(); + [Fact] + public async Task In_Use_Provider_Two_Named_Is_Not_Shutdown() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync("B", provider1, context); - await repository.SetProviderAsync("A", provider1, context); - // Provider one is replaced for "A", but not "B". - await repository.SetProviderAsync("A", provider2, context); + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); - provider1.DidNotReceive().ShutdownAsync(); - } + var context = new EvaluationContextBuilder().Build(); - [Fact] - public async Task When_All_Instances_Are_Removed_Shutdown_Is_Called() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); + await repository.SetProviderAsync("B", provider1, context); + await repository.SetProviderAsync("A", provider1, context); + // Provider one is replaced for "A", but not "B". + await repository.SetProviderAsync("A", provider2, context); - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); + provider1.DidNotReceive().ShutdownAsync(); + } - var context = new EvaluationContextBuilder().Build(); + [Fact] + public async Task When_All_Instances_Are_Removed_Shutdown_Is_Called() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync("B", provider1, context); - await repository.SetProviderAsync("A", provider1, context); + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync("A", provider2, context); - await repository.SetProviderAsync("B", provider2, context); + var context = new EvaluationContextBuilder().Build(); - provider1.Received(1).ShutdownAsync(); - } + await repository.SetProviderAsync("B", provider1, context); + await repository.SetProviderAsync("A", provider1, context); - [Fact] - public async Task Can_Get_Providers_By_Name() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); + await repository.SetProviderAsync("A", provider2, context); + await repository.SetProviderAsync("B", provider2, context); - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); + provider1.Received(1).ShutdownAsync(); + } - var context = new EvaluationContextBuilder().Build(); + [Fact] + public async Task Can_Get_Providers_By_Name() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync("A", provider1, context); - await repository.SetProviderAsync("B", provider2, context); + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); - Assert.Equal(provider1, repository.GetProvider("A")); - Assert.Equal(provider2, repository.GetProvider("B")); - } + var context = new EvaluationContextBuilder().Build(); - [Fact] - public async Task Replaced_Named_Provider_Gets_Latest_Set() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); + await repository.SetProviderAsync("A", provider1, context); + await repository.SetProviderAsync("B", provider2, context); - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); + Assert.Equal(provider1, repository.GetProvider("A")); + Assert.Equal(provider2, repository.GetProvider("B")); + } - var context = new EvaluationContextBuilder().Build(); + [Fact] + public async Task Replaced_Named_Provider_Gets_Latest_Set() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync("A", provider1, context); - await repository.SetProviderAsync("A", provider2, context); + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); - Assert.Equal(provider2, repository.GetProvider("A")); - } + var context = new EvaluationContextBuilder().Build(); - [Fact] - public async Task Can_Shutdown_All_Providers() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.Status.Returns(ProviderStatus.NotReady); + await repository.SetProviderAsync("A", provider1, context); + await repository.SetProviderAsync("A", provider2, context); - var provider2 = Substitute.For(); - provider2.Status.Returns(ProviderStatus.NotReady); + Assert.Equal(provider2, repository.GetProvider("A")); + } - var provider3 = Substitute.For(); - provider3.Status.Returns(ProviderStatus.NotReady); + [Fact] + public async Task Can_Shutdown_All_Providers() + { + var repository = new ProviderRepository(); + var provider1 = Substitute.For(); + provider1.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); + var provider2 = Substitute.For(); + provider2.Status.Returns(ProviderStatus.NotReady); - await repository.SetProviderAsync(provider1, context); - await repository.SetProviderAsync("provider1", provider1, context); - await repository.SetProviderAsync("provider2", provider2, context); - await repository.SetProviderAsync("provider2a", provider2, context); - await repository.SetProviderAsync("provider3", provider3, context); + var provider3 = Substitute.For(); + provider3.Status.Returns(ProviderStatus.NotReady); - await repository.ShutdownAsync(); + var context = new EvaluationContextBuilder().Build(); - provider1.Received(1).ShutdownAsync(); - provider2.Received(1).ShutdownAsync(); - provider3.Received(1).ShutdownAsync(); - } + await repository.SetProviderAsync(provider1, context); + await repository.SetProviderAsync("provider1", provider1, context); + await repository.SetProviderAsync("provider2", provider2, context); + await repository.SetProviderAsync("provider2a", provider2, context); + await repository.SetProviderAsync("provider3", provider3, context); - [Fact] - public async Task Setting_Same_Default_Provider_Has_No_Effect() - { - var repository = new ProviderRepository(); - var provider = Substitute.For(); - provider.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider, context); - await repository.SetProviderAsync(provider, context); - - Assert.Equal(provider, repository.GetProvider()); - provider.Received(1).InitializeAsync(context); - provider.DidNotReceive().ShutdownAsync(); - } - - [Fact] - public async Task Setting_Null_Default_Provider_Has_No_Effect() - { - var repository = new ProviderRepository(); - var provider = Substitute.For(); - provider.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider, context); - await repository.SetProviderAsync(null, context); - - Assert.Equal(provider, repository.GetProvider()); - provider.Received(1).InitializeAsync(context); - provider.DidNotReceive().ShutdownAsync(); - } - - [Fact] - public async Task Setting_Null_Named_Provider_Removes_It() - { - var repository = new ProviderRepository(); + await repository.ShutdownAsync(); + + provider1.Received(1).ShutdownAsync(); + provider2.Received(1).ShutdownAsync(); + provider3.Received(1).ShutdownAsync(); + } + + [Fact] + public async Task Setting_Same_Default_Provider_Has_No_Effect() + { + var repository = new ProviderRepository(); + var provider = Substitute.For(); + provider.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync(provider, context); + await repository.SetProviderAsync(provider, context); + + Assert.Equal(provider, repository.GetProvider()); + provider.Received(1).InitializeAsync(context); + provider.DidNotReceive().ShutdownAsync(); + } + + [Fact] + public async Task Setting_Null_Default_Provider_Has_No_Effect() + { + var repository = new ProviderRepository(); + var provider = Substitute.For(); + provider.Status.Returns(ProviderStatus.NotReady); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync(provider, context); + await repository.SetProviderAsync(null, context); + + Assert.Equal(provider, repository.GetProvider()); + provider.Received(1).InitializeAsync(context); + provider.DidNotReceive().ShutdownAsync(); + } + + [Fact] + public async Task Setting_Null_Named_Provider_Removes_It() + { + var repository = new ProviderRepository(); - var namedProvider = Substitute.For(); - namedProvider.Status.Returns(ProviderStatus.NotReady); + var namedProvider = Substitute.For(); + namedProvider.Status.Returns(ProviderStatus.NotReady); - var defaultProvider = Substitute.For(); - defaultProvider.Status.Returns(ProviderStatus.NotReady); + var defaultProvider = Substitute.For(); + defaultProvider.Status.Returns(ProviderStatus.NotReady); - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(defaultProvider, context); + var context = new EvaluationContextBuilder().Build(); + await repository.SetProviderAsync(defaultProvider, context); - await repository.SetProviderAsync("named-provider", namedProvider, context); - await repository.SetProviderAsync("named-provider", null, context); + await repository.SetProviderAsync("named-provider", namedProvider, context); + await repository.SetProviderAsync("named-provider", null, context); - Assert.Equal(defaultProvider, repository.GetProvider("named-provider")); - } + Assert.Equal(defaultProvider, repository.GetProvider("named-provider")); } } diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index c575dc56..8f1520a7 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -7,249 +7,248 @@ using OpenFeature.Providers.Memory; using Xunit; -namespace OpenFeature.Tests.Providers.Memory -{ - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class InMemoryProviderTests - { - private FeatureProvider commonProvider; - - public InMemoryProviderTests() - { - var provider = new InMemoryProvider(new Dictionary(){ - { - "boolean-flag", new Flag( - variants: new Dictionary(){ - { "on", true }, - { "off", false } - }, - defaultVariant: "on" - ) - }, - { - "string-flag", new Flag( - variants: new Dictionary(){ - { "greeting", "hi" }, - { "parting", "bye" } - }, - defaultVariant: "greeting" - ) - }, - { - "integer-flag", new Flag( - variants: new Dictionary(){ - { "one", 1 }, - { "ten", 10 } - }, - defaultVariant: "ten" - ) - }, - { - "float-flag", new Flag( - variants: new Dictionary(){ - { "tenth", 0.1 }, - { "half", 0.5 } - }, - defaultVariant: "half" - ) - }, - { - "context-aware", new Flag( - variants: new Dictionary(){ - { "internal", "INTERNAL" }, - { "external", "EXTERNAL" } - }, - defaultVariant: "external", - (context) => { - if (context.GetValue("email").AsString?.Contains("@faas.com") == true) - { - return "internal"; - } - else return "external"; - } - ) - }, - { - "object-flag", new Flag( - variants: new Dictionary(){ - { "empty", new Value() }, - { "template", new Value(Structure.Builder() - .Set("showImages", true) - .Set("title", "Check out these pics!") - .Set("imagesPerPage", 100).Build() - ) - } - }, - defaultVariant: "template" - ) - }, - { - "invalid-flag", new Flag( - variants: new Dictionary(){ - { "on", true }, - { "off", false } - }, - defaultVariant: "missing" - ) - }, - { - "invalid-evaluator-flag", new Flag( - variants: new Dictionary(){ - { "on", true }, - { "off", false } - }, - defaultVariant: "on", - (context) => { - return "missing"; - } - ) - } - }); - - this.commonProvider = provider; - } - - [Fact] - public async Task GetBoolean_ShouldEvaluateWithReasonAndVariant() - { - ResolutionDetails details = await this.commonProvider.ResolveBooleanValueAsync("boolean-flag", false, EvaluationContext.Empty); - Assert.True(details.Value); - Assert.Equal(Reason.Static, details.Reason); - Assert.Equal("on", details.Variant); - } - - [Fact] - public async Task GetString_ShouldEvaluateWithReasonAndVariant() - { - ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("string-flag", "nope", EvaluationContext.Empty); - Assert.Equal("hi", details.Value); - Assert.Equal(Reason.Static, details.Reason); - Assert.Equal("greeting", details.Variant); - } - - [Fact] - public async Task GetInt_ShouldEvaluateWithReasonAndVariant() - { - ResolutionDetails details = await this.commonProvider.ResolveIntegerValueAsync("integer-flag", 13, EvaluationContext.Empty); - Assert.Equal(10, details.Value); - Assert.Equal(Reason.Static, details.Reason); - Assert.Equal("ten", details.Variant); - } - - [Fact] - public async Task GetDouble_ShouldEvaluateWithReasonAndVariant() - { - ResolutionDetails details = await this.commonProvider.ResolveDoubleValueAsync("float-flag", 13, EvaluationContext.Empty); - Assert.Equal(0.5, details.Value); - Assert.Equal(Reason.Static, details.Reason); - Assert.Equal("half", details.Variant); - } - - [Fact] - public async Task GetStruct_ShouldEvaluateWithReasonAndVariant() - { - ResolutionDetails details = await this.commonProvider.ResolveStructureValueAsync("object-flag", new Value(), EvaluationContext.Empty); - Assert.Equal(true, details.Value.AsStructure?["showImages"].AsBoolean); - Assert.Equal("Check out these pics!", details.Value.AsStructure?["title"].AsString); - Assert.Equal(100, details.Value.AsStructure?["imagesPerPage"].AsInteger); - Assert.Equal(Reason.Static, details.Reason); - Assert.Equal("template", details.Variant); - } - - [Fact] - public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() - { - EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); - ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("context-aware", "nope", context); - Assert.Equal("INTERNAL", details.Value); - Assert.Equal(Reason.TargetingMatch, details.Reason); - Assert.Equal("internal", details.Variant); - } - - [Fact] - public async Task EmptyFlags_ShouldWork() - { - var provider = new InMemoryProvider(); - await provider.UpdateFlagsAsync(); - Assert.Equal("InMemory", provider.GetMetadata().Name); - } - - [Fact] - public async Task MissingFlag_ShouldReturnFlagNotFoundEvaluationFlag() - { - // Act - var result = await this.commonProvider.ResolveBooleanValueAsync("missing-flag", false, EvaluationContext.Empty); - - // Assert - Assert.Equal(Reason.Error, result.Reason); - Assert.Equal(ErrorType.FlagNotFound, result.ErrorType); - } - - [Fact] - public async Task MismatchedFlag_ShouldReturnTypeMismatchError() - { - // Act - var result = await this.commonProvider.ResolveStringValueAsync("boolean-flag", "nope", EvaluationContext.Empty); +namespace OpenFeature.Tests.Providers.Memory; - // Assert - Assert.Equal(Reason.Error, result.Reason); - Assert.Equal(ErrorType.TypeMismatch, result.ErrorType); - } - - [Fact] - public async Task MissingDefaultVariant_ShouldThrow() - { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-flag", false, EvaluationContext.Empty)); - } - - [Fact] - public async Task MissingEvaluatedVariant_ShouldThrow() - { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-evaluator-flag", false, EvaluationContext.Empty)); - } +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class InMemoryProviderTests +{ + private FeatureProvider commonProvider; - [Fact] - public async Task PutConfiguration_shouldUpdateConfigAndRunHandlers() - { - var provider = new InMemoryProvider(new Dictionary(){ + public InMemoryProviderTests() + { + var provider = new InMemoryProvider(new Dictionary(){ { - "old-flag", new Flag( + "boolean-flag", new Flag( variants: new Dictionary(){ { "on", true }, { "off", false } }, defaultVariant: "on" ) - }}); - - ResolutionDetails details = await provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty); - Assert.True(details.Value); - - // update flags - await provider.UpdateFlagsAsync(new Dictionary(){ + }, { - "new-flag", new Flag( + "string-flag", new Flag( variants: new Dictionary(){ { "greeting", "hi" }, { "parting", "bye" } }, defaultVariant: "greeting" ) - }}); + }, + { + "integer-flag", new Flag( + variants: new Dictionary(){ + { "one", 1 }, + { "ten", 10 } + }, + defaultVariant: "ten" + ) + }, + { + "float-flag", new Flag( + variants: new Dictionary(){ + { "tenth", 0.1 }, + { "half", 0.5 } + }, + defaultVariant: "half" + ) + }, + { + "context-aware", new Flag( + variants: new Dictionary(){ + { "internal", "INTERNAL" }, + { "external", "EXTERNAL" } + }, + defaultVariant: "external", + (context) => { + if (context.GetValue("email").AsString?.Contains("@faas.com") == true) + { + return "internal"; + } + else return "external"; + } + ) + }, + { + "object-flag", new Flag( + variants: new Dictionary(){ + { "empty", new Value() }, + { "template", new Value(Structure.Builder() + .Set("showImages", true) + .Set("title", "Check out these pics!") + .Set("imagesPerPage", 100).Build() + ) + } + }, + defaultVariant: "template" + ) + }, + { + "invalid-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "missing" + ) + }, + { + "invalid-evaluator-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on", + (context) => { + return "missing"; + } + ) + } + }); + + this.commonProvider = provider; + } + + [Fact] + public async Task GetBoolean_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveBooleanValueAsync("boolean-flag", false, EvaluationContext.Empty); + Assert.True(details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("on", details.Variant); + } + + [Fact] + public async Task GetString_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("string-flag", "nope", EvaluationContext.Empty); + Assert.Equal("hi", details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("greeting", details.Variant); + } + + [Fact] + public async Task GetInt_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveIntegerValueAsync("integer-flag", 13, EvaluationContext.Empty); + Assert.Equal(10, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("ten", details.Variant); + } + + [Fact] + public async Task GetDouble_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveDoubleValueAsync("float-flag", 13, EvaluationContext.Empty); + Assert.Equal(0.5, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("half", details.Variant); + } + + [Fact] + public async Task GetStruct_ShouldEvaluateWithReasonAndVariant() + { + ResolutionDetails details = await this.commonProvider.ResolveStructureValueAsync("object-flag", new Value(), EvaluationContext.Empty); + Assert.Equal(true, details.Value.AsStructure?["showImages"].AsBoolean); + Assert.Equal("Check out these pics!", details.Value.AsStructure?["title"].AsString); + Assert.Equal(100, details.Value.AsStructure?["imagesPerPage"].AsInteger); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("template", details.Variant); + } + + [Fact] + public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() + { + EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("context-aware", "nope", context); + Assert.Equal("INTERNAL", details.Value); + Assert.Equal(Reason.TargetingMatch, details.Reason); + Assert.Equal("internal", details.Variant); + } + + [Fact] + public async Task EmptyFlags_ShouldWork() + { + var provider = new InMemoryProvider(); + await provider.UpdateFlagsAsync(); + Assert.Equal("InMemory", provider.GetMetadata().Name); + } + + [Fact] + public async Task MissingFlag_ShouldReturnFlagNotFoundEvaluationFlag() + { + // Act + var result = await this.commonProvider.ResolveBooleanValueAsync("missing-flag", false, EvaluationContext.Empty); + + // Assert + Assert.Equal(Reason.Error, result.Reason); + Assert.Equal(ErrorType.FlagNotFound, result.ErrorType); + } + + [Fact] + public async Task MismatchedFlag_ShouldReturnTypeMismatchError() + { + // 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] + public async Task MissingDefaultVariant_ShouldThrow() + { + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-flag", false, EvaluationContext.Empty)); + } + + [Fact] + public async Task MissingEvaluatedVariant_ShouldThrow() + { + await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-evaluator-flag", false, EvaluationContext.Empty)); + } + + [Fact] + public async Task PutConfiguration_shouldUpdateConfigAndRunHandlers() + { + var provider = new InMemoryProvider(new Dictionary(){ + { + "old-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on" + ) + }}); + + ResolutionDetails details = await provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty); + Assert.True(details.Value); + + // update flags + await provider.UpdateFlagsAsync(new Dictionary(){ + { + "new-flag", new Flag( + variants: new Dictionary(){ + { "greeting", "hi" }, + { "parting", "bye" } + }, + defaultVariant: "greeting" + ) + }}); - var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; - Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res?.Type); + var res = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload; + Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res?.Type); - // old flag should be gone - var oldFlag = await provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty); + // old flag should be gone + var oldFlag = await provider.ResolveBooleanValueAsync("old-flag", false, EvaluationContext.Empty); - Assert.Equal(Reason.Error, oldFlag.Reason); - Assert.Equal(ErrorType.FlagNotFound, oldFlag.ErrorType); + Assert.Equal(Reason.Error, oldFlag.Reason); + Assert.Equal(ErrorType.FlagNotFound, oldFlag.ErrorType); - // new flag should be present, old gone (defaults), handler run. - ResolutionDetails detailsAfter = await provider.ResolveStringValueAsync("new-flag", "nope", EvaluationContext.Empty); - Assert.True(details.Value); - Assert.Equal("hi", detailsAfter.Value); - } + // new flag should be present, old gone (defaults), handler run. + ResolutionDetails detailsAfter = await provider.ResolveStringValueAsync("new-flag", "nope", EvaluationContext.Empty); + Assert.True(details.Value); + Assert.Equal("hi", detailsAfter.Value); } } diff --git a/test/OpenFeature.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs index 2dd22ae7..484e2b19 100644 --- a/test/OpenFeature.Tests/StructureTests.cs +++ b/test/OpenFeature.Tests/StructureTests.cs @@ -4,117 +4,116 @@ using OpenFeature.Model; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +public class StructureTests { - public class StructureTests + [Fact] + public void No_Arg_Should_Contain_Empty_Attributes() { - [Fact] - public void No_Arg_Should_Contain_Empty_Attributes() - { - Structure structure = Structure.Empty; - Assert.Equal(0, structure.Count); - Assert.Empty(structure.AsDictionary()); - } + Structure structure = Structure.Empty; + Assert.Equal(0, structure.Count); + Assert.Empty(structure.AsDictionary()); + } - [Fact] - public void Dictionary_Arg_Should_Contain_New_Dictionary() - { - string KEY = "key"; - IDictionary dictionary = new Dictionary() { { KEY, new Value(KEY) } }; - Structure structure = new Structure(dictionary); - Assert.Equal(KEY, structure.AsDictionary()[KEY].AsString); - Assert.NotSame(structure.AsDictionary(), dictionary); // should be a copy - } + [Fact] + public void Dictionary_Arg_Should_Contain_New_Dictionary() + { + string KEY = "key"; + IDictionary dictionary = new Dictionary() { { KEY, new Value(KEY) } }; + Structure structure = new Structure(dictionary); + Assert.Equal(KEY, structure.AsDictionary()[KEY].AsString); + Assert.NotSame(structure.AsDictionary(), dictionary); // should be a copy + } - [Fact] - public void Add_And_Get_Add_And_Return_Values() - { - String BOOL_KEY = "bool"; - String STRING_KEY = "string"; - String INT_KEY = "int"; - String DOUBLE_KEY = "double"; - String DATE_KEY = "date"; - String STRUCT_KEY = "struct"; - String LIST_KEY = "list"; - String VALUE_KEY = "value"; + [Fact] + public void Add_And_Get_Add_And_Return_Values() + { + String BOOL_KEY = "bool"; + String STRING_KEY = "string"; + String INT_KEY = "int"; + String DOUBLE_KEY = "double"; + String DATE_KEY = "date"; + String STRUCT_KEY = "struct"; + String LIST_KEY = "list"; + String VALUE_KEY = "value"; - bool BOOL_VAL = true; - String STRING_VAL = "val"; - int INT_VAL = 13; - double DOUBLE_VAL = .5; - DateTime DATE_VAL = DateTime.Now; - Structure STRUCT_VAL = Structure.Empty; - IList LIST_VAL = new List(); - Value VALUE_VAL = new Value(); + bool BOOL_VAL = true; + String STRING_VAL = "val"; + int INT_VAL = 13; + double DOUBLE_VAL = .5; + DateTime DATE_VAL = DateTime.Now; + Structure STRUCT_VAL = Structure.Empty; + IList LIST_VAL = new List(); + Value VALUE_VAL = new Value(); - var structureBuilder = Structure.Builder(); - structureBuilder.Set(BOOL_KEY, BOOL_VAL); - structureBuilder.Set(STRING_KEY, STRING_VAL); - structureBuilder.Set(INT_KEY, INT_VAL); - structureBuilder.Set(DOUBLE_KEY, DOUBLE_VAL); - structureBuilder.Set(DATE_KEY, DATE_VAL); - structureBuilder.Set(STRUCT_KEY, STRUCT_VAL); - structureBuilder.Set(LIST_KEY, ImmutableList.CreateRange(LIST_VAL)); - structureBuilder.Set(VALUE_KEY, VALUE_VAL); - var structure = structureBuilder.Build(); + var structureBuilder = Structure.Builder(); + structureBuilder.Set(BOOL_KEY, BOOL_VAL); + structureBuilder.Set(STRING_KEY, STRING_VAL); + structureBuilder.Set(INT_KEY, INT_VAL); + structureBuilder.Set(DOUBLE_KEY, DOUBLE_VAL); + structureBuilder.Set(DATE_KEY, DATE_VAL); + structureBuilder.Set(STRUCT_KEY, STRUCT_VAL); + structureBuilder.Set(LIST_KEY, ImmutableList.CreateRange(LIST_VAL)); + structureBuilder.Set(VALUE_KEY, VALUE_VAL); + var structure = structureBuilder.Build(); - Assert.Equal(BOOL_VAL, structure.GetValue(BOOL_KEY).AsBoolean); - Assert.Equal(STRING_VAL, structure.GetValue(STRING_KEY).AsString); - Assert.Equal(INT_VAL, structure.GetValue(INT_KEY).AsInteger); - Assert.Equal(DOUBLE_VAL, structure.GetValue(DOUBLE_KEY).AsDouble); - Assert.Equal(DATE_VAL, structure.GetValue(DATE_KEY).AsDateTime); - Assert.Equal(STRUCT_VAL, structure.GetValue(STRUCT_KEY).AsStructure); - Assert.Equal(LIST_VAL, structure.GetValue(LIST_KEY).AsList); - Assert.True(structure.GetValue(VALUE_KEY).IsNull); - } + Assert.Equal(BOOL_VAL, structure.GetValue(BOOL_KEY).AsBoolean); + Assert.Equal(STRING_VAL, structure.GetValue(STRING_KEY).AsString); + Assert.Equal(INT_VAL, structure.GetValue(INT_KEY).AsInteger); + Assert.Equal(DOUBLE_VAL, structure.GetValue(DOUBLE_KEY).AsDouble); + Assert.Equal(DATE_VAL, structure.GetValue(DATE_KEY).AsDateTime); + Assert.Equal(STRUCT_VAL, structure.GetValue(STRUCT_KEY).AsStructure); + Assert.Equal(LIST_VAL, structure.GetValue(LIST_KEY).AsList); + Assert.True(structure.GetValue(VALUE_KEY).IsNull); + } - [Fact] - public void TryGetValue_Should_Return_Value() - { - String KEY = "key"; - String VAL = "val"; + [Fact] + public void TryGetValue_Should_Return_Value() + { + String KEY = "key"; + String VAL = "val"; - var structure = Structure.Builder() - .Set(KEY, VAL).Build(); - Value? value; - Assert.True(structure.TryGetValue(KEY, out value)); - Assert.Equal(VAL, value?.AsString); - } + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); + Value? value; + Assert.True(structure.TryGetValue(KEY, out value)); + Assert.Equal(VAL, value?.AsString); + } - [Fact] - public void Values_Should_Return_Values() - { - String KEY = "key"; - Value VAL = new Value("val"); + [Fact] + public void Values_Should_Return_Values() + { + String KEY = "key"; + Value VAL = new Value("val"); - var structure = Structure.Builder() - .Set(KEY, VAL).Build(); - Assert.Single(structure.Values); - } + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); + Assert.Single(structure.Values); + } - [Fact] - public void Keys_Should_Return_Keys() - { - String KEY = "key"; - Value VAL = new Value("val"); + [Fact] + public void Keys_Should_Return_Keys() + { + String KEY = "key"; + Value VAL = new Value("val"); - var structure = Structure.Builder() - .Set(KEY, VAL).Build(); - Assert.Single(structure.Keys); - Assert.Equal(0, structure.Keys.IndexOf(KEY)); - } + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); + Assert.Single(structure.Keys); + Assert.Equal(0, structure.Keys.IndexOf(KEY)); + } - [Fact] - public void GetEnumerator_Should_Return_Enumerator() - { - string KEY = "key"; - string VAL = "val"; + [Fact] + public void GetEnumerator_Should_Return_Enumerator() + { + string KEY = "key"; + string VAL = "val"; - var structure = Structure.Builder() - .Set(KEY, VAL).Build(); - IEnumerator> enumerator = structure.GetEnumerator(); - enumerator.MoveNext(); - Assert.Equal(VAL, enumerator.Current.Value.AsString); - } + var structure = Structure.Builder() + .Set(KEY, VAL).Build(); + IEnumerator> enumerator = structure.GetEnumerator(); + enumerator.MoveNext(); + Assert.Equal(VAL, enumerator.Current.Value.AsString); } } diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index df738efe..4c298c88 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -6,151 +6,150 @@ using OpenFeature.Constant; using OpenFeature.Model; -namespace OpenFeature.Tests -{ - public class TestHookNoOverride : Hook - { - } +namespace OpenFeature.Tests; - public class TestHook : Hook - { - private int _beforeCallCount; - public int BeforeCallCount { get => this._beforeCallCount; } +public class TestHookNoOverride : Hook +{ +} - private int _afterCallCount; - public int AfterCallCount { get => this._afterCallCount; } +public class TestHook : Hook +{ + private int _beforeCallCount; + public int BeforeCallCount { get => this._beforeCallCount; } - private int _errorCallCount; - public int ErrorCallCount { get => this._errorCallCount; } + private int _afterCallCount; + public int AfterCallCount { get => this._afterCallCount; } - private int _finallyCallCount; - public int FinallyCallCount { get => this._finallyCallCount; } + private int _errorCallCount; + public int ErrorCallCount { get => this._errorCallCount; } - public override ValueTask BeforeAsync(HookContext context, - IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) - { - Interlocked.Increment(ref this._beforeCallCount); - return new ValueTask(EvaluationContext.Empty); - } + private int _finallyCallCount; + public int FinallyCallCount { get => this._finallyCallCount; } - public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) - { - Interlocked.Increment(ref this._afterCallCount); - return new ValueTask(); - } + public override ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref this._beforeCallCount); + return new ValueTask(EvaluationContext.Empty); + } - public override ValueTask ErrorAsync(HookContext context, Exception error, - IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) - { - Interlocked.Increment(ref this._errorCallCount); - return new ValueTask(); - } + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref this._afterCallCount); + return new ValueTask(); + } - public override ValueTask FinallyAsync(HookContext context, - FlagEvaluationDetails evaluationDetails, - IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) - { - Interlocked.Increment(ref this._finallyCallCount); - return new ValueTask(); - } + public override ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref this._errorCallCount); + return new ValueTask(); } - public class TestProvider : FeatureProvider + public override ValueTask FinallyAsync(HookContext context, + FlagEvaluationDetails evaluationDetails, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) { - private readonly List _hooks = new List(); + Interlocked.Increment(ref this._finallyCallCount); + return new ValueTask(); + } +} - public static string DefaultName = "test-provider"; - private readonly List> TrackingInvocations = []; +public class TestProvider : FeatureProvider +{ + private readonly List _hooks = new List(); - public string? Name { get; set; } + public static string DefaultName = "test-provider"; + private readonly List> TrackingInvocations = []; - public void AddHook(Hook hook) => this._hooks.Add(hook); + public string? Name { get; set; } - public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList(); - private Exception? initException = null; - private int initDelay = 0; + public void AddHook(Hook hook) => this._hooks.Add(hook); - public TestProvider() - { - this.Name = DefaultName; - } + public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList(); + private Exception? initException = null; + private int initDelay = 0; - /// - /// A provider used for testing. - /// - /// the name of the provider. - /// Optional exception to throw during init. - /// - public TestProvider(string? name, Exception? initException = null, int initDelay = 0) - { - this.Name = string.IsNullOrEmpty(name) ? DefaultName : name; - this.initException = initException; - this.initDelay = initDelay; - } + public TestProvider() + { + this.Name = DefaultName; + } - public ImmutableList> GetTrackingInvocations() - { - return this.TrackingInvocations.ToImmutableList(); - } + /// + /// A provider used for testing. + /// + /// the name of the provider. + /// Optional exception to throw during init. + /// + public TestProvider(string? name, Exception? initException = null, int initDelay = 0) + { + this.Name = string.IsNullOrEmpty(name) ? DefaultName : name; + this.initException = initException; + this.initDelay = initDelay; + } - public void Reset() - { - this.TrackingInvocations.Clear(); - } + public ImmutableList> GetTrackingInvocations() + { + return this.TrackingInvocations.ToImmutableList(); + } - public override Metadata GetMetadata() - { - return new Metadata(this.Name); - } + public void Reset() + { + this.TrackingInvocations.Clear(); + } - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, - EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); - } + public override Metadata GetMetadata() + { + return new Metadata(this.Name); + } - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, - EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); - } + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(flagKey, !defaultValue)); + } - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, - EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); - } + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, - EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); - } + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, - EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); - } + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } - public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) - { - await Task.Delay(this.initDelay).ConfigureAwait(false); - if (this.initException != null) - { - throw this.initException; - } - } + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } - public override void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default) + public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) + { + await Task.Delay(this.initDelay).ConfigureAwait(false); + if (this.initException != null) { - this.TrackingInvocations.Add(new Tuple(trackingEventName, evaluationContext, trackingEventDetails)); + throw this.initException; } + } - internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) - { - return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name, }, cancellationToken); - } + public override void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default) + { + this.TrackingInvocations.Add(new Tuple(trackingEventName, evaluationContext, trackingEventDetails)); + } + + internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) + { + return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name, }, cancellationToken); } } diff --git a/test/OpenFeature.Tests/TestUtilsTest.cs b/test/OpenFeature.Tests/TestUtilsTest.cs index b65a91f5..9f5cde86 100644 --- a/test/OpenFeature.Tests/TestUtilsTest.cs +++ b/test/OpenFeature.Tests/TestUtilsTest.cs @@ -3,21 +3,20 @@ using System.Threading.Tasks; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] +public class TestUtilsTest { - [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class TestUtilsTest + [Fact] + public async Task Should_Fail_If_Assertion_Fails() { - [Fact] - public async Task Should_Fail_If_Assertion_Fails() - { - await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)); - } + await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)); + } - [Fact] - public async Task Should_Pass_If_Assertion_Fails() - { - await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))); - } + [Fact] + public async Task Should_Pass_If_Assertion_Fails() + { + await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))); } } diff --git a/test/OpenFeature.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs index ec623a68..34a2eb6b 100644 --- a/test/OpenFeature.Tests/ValueTests.cs +++ b/test/OpenFeature.Tests/ValueTests.cs @@ -3,235 +3,234 @@ using OpenFeature.Model; using Xunit; -namespace OpenFeature.Tests +namespace OpenFeature.Tests; + +public class ValueTests { - public class ValueTests + class Foo { - class Foo - { - } - - [Fact] - public void No_Arg_Should_Contain_Null() - { - Value value = new Value(); - Assert.True(value.IsNull); - } + } - [Fact] - public void Object_Arg_Should_Contain_Object() - { - // int is a special case, see Int_Object_Arg_Should_Contain_Object() - IList list = new List() - { - true, - "val", - .5, - Structure.Empty, - new List(), - DateTime.Now - }; - - int i = 0; - foreach (Object l in list) - { - Value value = new Value(l); - Assert.Equal(list[i], value.AsObject); - i++; - } - } + [Fact] + public void No_Arg_Should_Contain_Null() + { + Value value = new Value(); + Assert.True(value.IsNull); + } - [Fact] - public void Int_Object_Arg_Should_Contain_Object() + [Fact] + public void Object_Arg_Should_Contain_Object() + { + // int is a special case, see Int_Object_Arg_Should_Contain_Object() + IList list = new List() { - try - { - int innerValue = 1; - Value value = new Value(innerValue); - Assert.True(value.IsNumber); - Assert.Equal(innerValue, value.AsInteger); - } - catch (Exception) - { - Assert.Fail("Expected no exception."); - } - } + true, + "val", + .5, + Structure.Empty, + new List(), + DateTime.Now + }; - [Fact] - public void Invalid_Object_Should_Throw() + int i = 0; + foreach (Object l in list) { - Assert.Throws(() => - { - return new Value(new Foo()); - }); + Value value = new Value(l); + Assert.Equal(list[i], value.AsObject); + i++; } + } - [Fact] - public void Bool_Arg_Should_Contain_Bool() + [Fact] + public void Int_Object_Arg_Should_Contain_Object() + { + try { - bool innerValue = true; + int innerValue = 1; Value value = new Value(innerValue); - Assert.True(value.IsBoolean); - Assert.Equal(innerValue, value.AsBoolean); + Assert.True(value.IsNumber); + Assert.Equal(innerValue, value.AsInteger); } - - [Fact] - public void Numeric_Arg_Should_Return_Double_Or_Int() + catch (Exception) { - double innerDoubleValue = .75; - Value doubleValue = new Value(innerDoubleValue); - Assert.True(doubleValue.IsNumber); - Assert.Equal(1, doubleValue.AsInteger); // should be rounded - Assert.Equal(.75, doubleValue.AsDouble); - - int innerIntValue = 100; - Value intValue = new Value(innerIntValue); - Assert.True(intValue.IsNumber); - Assert.Equal(innerIntValue, intValue.AsInteger); - Assert.Equal(innerIntValue, intValue.AsDouble); + Assert.Fail("Expected no exception."); } + } - [Fact] - public void String_Arg_Should_Contain_String() + [Fact] + public void Invalid_Object_Should_Throw() + { + Assert.Throws(() => { - string innerValue = "hi!"; - Value value = new Value(innerValue); - Assert.True(value.IsString); - Assert.Equal(innerValue, value.AsString); - } + return new Value(new Foo()); + }); + } - [Fact] - public void DateTime_Arg_Should_Contain_DateTime() - { - DateTime innerValue = new DateTime(); - Value value = new Value(innerValue); - Assert.True(value.IsDateTime); - Assert.Equal(innerValue, value.AsDateTime); - } + [Fact] + public void Bool_Arg_Should_Contain_Bool() + { + bool innerValue = true; + Value value = new Value(innerValue); + Assert.True(value.IsBoolean); + Assert.Equal(innerValue, value.AsBoolean); + } - [Fact] - public void Structure_Arg_Should_Contain_Structure() - { - string INNER_KEY = "key"; - string INNER_VALUE = "val"; - Structure innerValue = Structure.Builder().Set(INNER_KEY, INNER_VALUE).Build(); - Value value = new Value(innerValue); - Assert.True(value.IsStructure); - Assert.Equal(INNER_VALUE, value.AsStructure?.GetValue(INNER_KEY).AsString); - } + [Fact] + public void Numeric_Arg_Should_Return_Double_Or_Int() + { + double innerDoubleValue = .75; + Value doubleValue = new Value(innerDoubleValue); + Assert.True(doubleValue.IsNumber); + Assert.Equal(1, doubleValue.AsInteger); // should be rounded + Assert.Equal(.75, doubleValue.AsDouble); + + int innerIntValue = 100; + Value intValue = new Value(innerIntValue); + Assert.True(intValue.IsNumber); + Assert.Equal(innerIntValue, intValue.AsInteger); + Assert.Equal(innerIntValue, intValue.AsDouble); + } - [Fact] - public void List_Arg_Should_Contain_List() - { - string ITEM_VALUE = "val"; - IList innerValue = new List() { new Value(ITEM_VALUE) }; - Value value = new Value(innerValue); - Assert.True(value.IsList); - Assert.Equal(ITEM_VALUE, value.AsList?[0].AsString); - } + [Fact] + public void String_Arg_Should_Contain_String() + { + string innerValue = "hi!"; + Value value = new Value(innerValue); + Assert.True(value.IsString); + Assert.Equal(innerValue, value.AsString); + } - [Fact] - public void Constructor_WhenCalledWithAnotherValue_CopiesInnerValue() - { - // Arrange - var originalValue = new Value("testValue"); + [Fact] + public void DateTime_Arg_Should_Contain_DateTime() + { + DateTime innerValue = new DateTime(); + Value value = new Value(innerValue); + Assert.True(value.IsDateTime); + Assert.Equal(innerValue, value.AsDateTime); + } - // Act - var copiedValue = new Value(originalValue); + [Fact] + public void Structure_Arg_Should_Contain_Structure() + { + string INNER_KEY = "key"; + string INNER_VALUE = "val"; + Structure innerValue = Structure.Builder().Set(INNER_KEY, INNER_VALUE).Build(); + Value value = new Value(innerValue); + Assert.True(value.IsStructure); + Assert.Equal(INNER_VALUE, value.AsStructure?.GetValue(INNER_KEY).AsString); + } - // Assert - Assert.Equal(originalValue.AsObject, copiedValue.AsObject); - } + [Fact] + public void List_Arg_Should_Contain_List() + { + string ITEM_VALUE = "val"; + IList innerValue = new List() { new Value(ITEM_VALUE) }; + Value value = new Value(innerValue); + Assert.True(value.IsList); + Assert.Equal(ITEM_VALUE, value.AsList?[0].AsString); + } - [Fact] - public void AsInteger_WhenCalledWithNonIntegerInnerValue_ReturnsNull() - { - // Arrange - var value = new Value("test"); + [Fact] + public void Constructor_WhenCalledWithAnotherValue_CopiesInnerValue() + { + // Arrange + var originalValue = new Value("testValue"); - // Act - var actualValue = value.AsInteger; + // Act + var copiedValue = new Value(originalValue); - // Assert - Assert.Null(actualValue); - } + // Assert + Assert.Equal(originalValue.AsObject, copiedValue.AsObject); + } - [Fact] - public void AsBoolean_WhenCalledWithNonBooleanInnerValue_ReturnsNull() - { - // Arrange - var value = new Value("test"); + [Fact] + public void AsInteger_WhenCalledWithNonIntegerInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); - // Act - var actualValue = value.AsBoolean; + // Act + var actualValue = value.AsInteger; - // Assert - Assert.Null(actualValue); - } + // Assert + Assert.Null(actualValue); + } - [Fact] - public void AsDouble_WhenCalledWithNonDoubleInnerValue_ReturnsNull() - { - // Arrange - var value = new Value("test"); + [Fact] + public void AsBoolean_WhenCalledWithNonBooleanInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); - // Act - var actualValue = value.AsDouble; + // Act + var actualValue = value.AsBoolean; - // Assert - Assert.Null(actualValue); - } + // Assert + Assert.Null(actualValue); + } - [Fact] - public void AsString_WhenCalledWithNonStringInnerValue_ReturnsNull() - { - // Arrange - var value = new Value(123); + [Fact] + public void AsDouble_WhenCalledWithNonDoubleInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); - // Act - var actualValue = value.AsString; + // Act + var actualValue = value.AsDouble; - // Assert - Assert.Null(actualValue); - } + // Assert + Assert.Null(actualValue); + } - [Fact] - public void AsStructure_WhenCalledWithNonStructureInnerValue_ReturnsNull() - { - // Arrange - var value = new Value("test"); + [Fact] + public void AsString_WhenCalledWithNonStringInnerValue_ReturnsNull() + { + // Arrange + var value = new Value(123); - // Act - var actualValue = value.AsStructure; + // Act + var actualValue = value.AsString; - // Assert - Assert.Null(actualValue); - } + // Assert + Assert.Null(actualValue); + } - [Fact] - public void AsList_WhenCalledWithNonListInnerValue_ReturnsNull() - { - // Arrange - var value = new Value("test"); + [Fact] + public void AsStructure_WhenCalledWithNonStructureInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); - // Act - var actualValue = value.AsList; + // Act + var actualValue = value.AsStructure; - // Assert - Assert.Null(actualValue); - } + // Assert + Assert.Null(actualValue); + } - [Fact] - public void AsDateTime_WhenCalledWithNonDateTimeInnerValue_ReturnsNull() - { - // Arrange - var value = new Value("test"); + [Fact] + public void AsList_WhenCalledWithNonListInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); - // Act - var actualValue = value.AsDateTime; + // Act + var actualValue = value.AsList; - // Assert - Assert.Null(actualValue); - } + // Assert + Assert.Null(actualValue); + } + + [Fact] + public void AsDateTime_WhenCalledWithNonDateTimeInnerValue_ReturnsNull() + { + // Arrange + var value = new Value("test"); + + // Act + var actualValue = value.AsDateTime; + + // Assert + Assert.Null(actualValue); } } From eb688c412983511c7ec0744df95e4a113f610c5f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:42:46 +0100 Subject: [PATCH 307/316] chore(deps): update spec digest to 2ba05d8 (#452) 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 | `36944c6` -> `2ba05d8` | --- ### 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 36944c68..2ba05d89 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 36944c68dd60e874661f5efd022ccafb9af76535 +Subproject commit 2ba05d89b5139fbe247018049e0bbff4e584463e From 42ab5368d3d8f874f175ab9ad3077f177a592398 Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Fri, 25 Apr 2025 07:07:48 +0100 Subject: [PATCH 308/316] chore: add NuGet auditing (#454) ## This PR - NuGet offers built in functionality for analyzing packages that are included in a software project. These changes will ensure msbuild outputs warnings when dependencies are flagged up with vulnerabilities. ### Related Issues Fixes #444 ### Notes ### Follow-up Tasks ### How to test Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --- build/Common.props | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/Common.props b/build/Common.props index 5cc12515..1c769bcf 100644 --- a/build/Common.props +++ b/build/Common.props @@ -8,6 +8,9 @@ EnableGenerateDocumentationFile enable true + true + all + low From e0ec8ca28303b7df71699063b02b6967cdc37bcd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:22:28 +0100 Subject: [PATCH 309/316] chore(deps): update spec digest to d27e000 (#455) 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 | `2ba05d8` -> `d27e000` | --- ### 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 2ba05d89..d27e000b 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 2ba05d89b5139fbe247018049e0bbff4e584463e +Subproject commit d27e000b6c839b533ff4f3ea0f5b1bfc024fb534 From 7318b8126df9f0ddd5651fdd9fe32da2e4819290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:11:34 +0100 Subject: [PATCH 310/316] docs: Update README with spec version (#437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request includes a small change to the `README.md` file. The change updates the specification badge to reflect the new version 0.8.0. ### Related Issues Fixes #204 Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3d6ae98..daad6ad4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.8.0) [ ![Release](https://img.shields.io/static/v1?label=release&message=v2.4.0&color=blue&style=for-the-badge) ](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.4.0) From 0821b3b0b8ffb45a16c1fe2e8bc0d1a8cc0e8c9f Mon Sep 17 00:00:00 2001 From: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:16:28 -0400 Subject: [PATCH 311/316] chore(main): release 2.5.0 (#435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.5.0](https://github.com/open-feature/dotnet-sdk/compare/v2.4.0...v2.5.0) (2025-04-25) ### ✨ New Features * Add support for hook data. ([#387](https://github.com/open-feature/dotnet-sdk/issues/387)) ([4563512](https://github.com/open-feature/dotnet-sdk/commit/456351216ce9113d84b56d0bce1dad39430a26cd)) ### 🧹 Chore * add NuGet auditing ([#454](https://github.com/open-feature/dotnet-sdk/issues/454)) ([42ab536](https://github.com/open-feature/dotnet-sdk/commit/42ab5368d3d8f874f175ab9ad3077f177a592398)) * Change file scoped namespaces and cleanup job ([#453](https://github.com/open-feature/dotnet-sdk/issues/453)) ([1e74a04](https://github.com/open-feature/dotnet-sdk/commit/1e74a04f2b76c128a09c95dfd0b06803f2ef77bf)) * **deps:** update codecov/codecov-action action to v5.4.2 ([#432](https://github.com/open-feature/dotnet-sdk/issues/432)) ([c692ec2](https://github.com/open-feature/dotnet-sdk/commit/c692ec2a26eb4007ff428e54eaa67ea22fd20728)) * **deps:** update github/codeql-action digest to 28deaed ([#446](https://github.com/open-feature/dotnet-sdk/issues/446)) ([dfecd0c](https://github.com/open-feature/dotnet-sdk/commit/dfecd0c6a4467e5c1afe481e785e3e0f179beb25)) * **deps:** update spec digest to 18cde17 ([#395](https://github.com/open-feature/dotnet-sdk/issues/395)) ([5608dfb](https://github.com/open-feature/dotnet-sdk/commit/5608dfbd441b99531add8e89ad842ea9d613f707)) * **deps:** update spec digest to 2ba05d8 ([#452](https://github.com/open-feature/dotnet-sdk/issues/452)) ([eb688c4](https://github.com/open-feature/dotnet-sdk/commit/eb688c412983511c7ec0744df95e4a113f610c5f)) * **deps:** update spec digest to 36944c6 ([#450](https://github.com/open-feature/dotnet-sdk/issues/450)) ([e162169](https://github.com/open-feature/dotnet-sdk/commit/e162169af0b5518f12527a8601d6dfcdf379b4f7)) * **deps:** update spec digest to d27e000 ([#455](https://github.com/open-feature/dotnet-sdk/issues/455)) ([e0ec8ca](https://github.com/open-feature/dotnet-sdk/commit/e0ec8ca28303b7df71699063b02b6967cdc37bcd)) * packages read in release please ([1acc00f](https://github.com/open-feature/dotnet-sdk/commit/1acc00fa7a6a38152d97fd7efc9f7e8befb1c3ed)) * update release permissions ([d0bf40b](https://github.com/open-feature/dotnet-sdk/commit/d0bf40b9b40adc57a2a008a9497098b3cd1a05a7)) * **workflows:** Add permissions for contents and pull-requests ([#439](https://github.com/open-feature/dotnet-sdk/issues/439)) ([568722a](https://github.com/open-feature/dotnet-sdk/commit/568722a4ab1f863d8509dc4a172ac9c29f267825)) ### πŸ“š Documentation * update documentation on SetProviderAsync ([#449](https://github.com/open-feature/dotnet-sdk/issues/449)) ([858b286](https://github.com/open-feature/dotnet-sdk/commit/858b286dba2313239141c20ec6770504d340fbe0)) * Update README with spec version ([#437](https://github.com/open-feature/dotnet-sdk/issues/437)) ([7318b81](https://github.com/open-feature/dotnet-sdk/commit/7318b8126df9f0ddd5651fdd9fe32da2e4819290)), closes [#204](https://github.com/open-feature/dotnet-sdk/issues/204) ### πŸ”„ Refactoring * InMemoryProvider throwing when types mismatched ([#442](https://github.com/open-feature/dotnet-sdk/issues/442)) ([8ecf50d](https://github.com/open-feature/dotnet-sdk/commit/8ecf50db2cab3a266de5c6c5216714570cfc6a52)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a549f59d..78baf5bf 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.4.0" + ".": "2.5.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1219039f..beebbd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## [2.5.0](https://github.com/open-feature/dotnet-sdk/compare/v2.4.0...v2.5.0) (2025-04-25) + + +### ✨ New Features + +* Add support for hook data. ([#387](https://github.com/open-feature/dotnet-sdk/issues/387)) ([4563512](https://github.com/open-feature/dotnet-sdk/commit/456351216ce9113d84b56d0bce1dad39430a26cd)) + + +### 🧹 Chore + +* add NuGet auditing ([#454](https://github.com/open-feature/dotnet-sdk/issues/454)) ([42ab536](https://github.com/open-feature/dotnet-sdk/commit/42ab5368d3d8f874f175ab9ad3077f177a592398)) +* Change file scoped namespaces and cleanup job ([#453](https://github.com/open-feature/dotnet-sdk/issues/453)) ([1e74a04](https://github.com/open-feature/dotnet-sdk/commit/1e74a04f2b76c128a09c95dfd0b06803f2ef77bf)) +* **deps:** update codecov/codecov-action action to v5.4.2 ([#432](https://github.com/open-feature/dotnet-sdk/issues/432)) ([c692ec2](https://github.com/open-feature/dotnet-sdk/commit/c692ec2a26eb4007ff428e54eaa67ea22fd20728)) +* **deps:** update github/codeql-action digest to 28deaed ([#446](https://github.com/open-feature/dotnet-sdk/issues/446)) ([dfecd0c](https://github.com/open-feature/dotnet-sdk/commit/dfecd0c6a4467e5c1afe481e785e3e0f179beb25)) +* **deps:** update spec digest to 18cde17 ([#395](https://github.com/open-feature/dotnet-sdk/issues/395)) ([5608dfb](https://github.com/open-feature/dotnet-sdk/commit/5608dfbd441b99531add8e89ad842ea9d613f707)) +* **deps:** update spec digest to 2ba05d8 ([#452](https://github.com/open-feature/dotnet-sdk/issues/452)) ([eb688c4](https://github.com/open-feature/dotnet-sdk/commit/eb688c412983511c7ec0744df95e4a113f610c5f)) +* **deps:** update spec digest to 36944c6 ([#450](https://github.com/open-feature/dotnet-sdk/issues/450)) ([e162169](https://github.com/open-feature/dotnet-sdk/commit/e162169af0b5518f12527a8601d6dfcdf379b4f7)) +* **deps:** update spec digest to d27e000 ([#455](https://github.com/open-feature/dotnet-sdk/issues/455)) ([e0ec8ca](https://github.com/open-feature/dotnet-sdk/commit/e0ec8ca28303b7df71699063b02b6967cdc37bcd)) +* packages read in release please ([1acc00f](https://github.com/open-feature/dotnet-sdk/commit/1acc00fa7a6a38152d97fd7efc9f7e8befb1c3ed)) +* update release permissions ([d0bf40b](https://github.com/open-feature/dotnet-sdk/commit/d0bf40b9b40adc57a2a008a9497098b3cd1a05a7)) +* **workflows:** Add permissions for contents and pull-requests ([#439](https://github.com/open-feature/dotnet-sdk/issues/439)) ([568722a](https://github.com/open-feature/dotnet-sdk/commit/568722a4ab1f863d8509dc4a172ac9c29f267825)) + + +### πŸ“š Documentation + +* update documentation on SetProviderAsync ([#449](https://github.com/open-feature/dotnet-sdk/issues/449)) ([858b286](https://github.com/open-feature/dotnet-sdk/commit/858b286dba2313239141c20ec6770504d340fbe0)) +* Update README with spec version ([#437](https://github.com/open-feature/dotnet-sdk/issues/437)) ([7318b81](https://github.com/open-feature/dotnet-sdk/commit/7318b8126df9f0ddd5651fdd9fe32da2e4819290)), closes [#204](https://github.com/open-feature/dotnet-sdk/issues/204) + + +### πŸ”„ Refactoring + +* InMemoryProvider throwing when types mismatched ([#442](https://github.com/open-feature/dotnet-sdk/issues/442)) ([8ecf50d](https://github.com/open-feature/dotnet-sdk/commit/8ecf50db2cab3a266de5c6c5216714570cfc6a52)) + ## [2.4.0](https://github.com/open-feature/dotnet-sdk/compare/v2.3.2...v2.4.0) (2025-04-14) diff --git a/README.md b/README.md index daad6ad4..3e51a690 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.8.0) [ -![Release](https://img.shields.io/static/v1?label=release&message=v2.4.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.4.0) +![Release](https://img.shields.io/static/v1?label=release&message=v2.5.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.5.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index 92b0ccb5..3f4ba37d 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.4.0 + 2.5.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 197c4d5c..437459cd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.4.0 +2.5.0 From 6a8b00aaa56347e897d4bb6abb930d46ac3443d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:20:06 +0100 Subject: [PATCH 312/316] ci: Move CODEOWNERS (#458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request removes the `.config/dotnet-tools.json` file, which previously specified the `dotnet-format` tool and its configuration. Key change: * [`.config/dotnet-tools.json`](diffhunk://#diff-7afd3bcf0d0c06d6f87c451ef06b321beef770ece4299b3230bb280470ada2f6L1-L12): Deleted the file, which contained configuration for the `dotnet-format` tool, including its version (`5.1.250801`) and associated commands. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- .config/dotnet-tools.json | 12 ------------ CODEOWNERS => .github/CODEOWNERS | 0 2 files changed, 12 deletions(-) delete mode 100644 .config/dotnet-tools.json rename CODEOWNERS => .github/CODEOWNERS (100%) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index a02f5dc5..00000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "dotnet-format": { - "version": "5.1.250801", - "commands": [ - "dotnet-format" - ] - } - } -} diff --git a/CODEOWNERS b/.github/CODEOWNERS similarity index 100% rename from CODEOWNERS rename to .github/CODEOWNERS From 8e3ae54489a5a275124b7c22d73142bbeedee8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 2 May 2025 20:33:52 +0100 Subject: [PATCH 313/316] build: Tidy build tests (#460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This pull request primarily addresses warnings related to the use of `ConfigureAwait` in test code. It suppresses these warnings globally for test projects and removes redundant `[SuppressMessage]` attributes from individual test classes. ### Suppression of `ConfigureAwait` warnings: * [`build/Common.tests.props`](diffhunk://#diff-5472aa271be4e6ac0c793a3c1b9226e4f9a7907a6baa99ea16542fb89107ae86R21-R25): Added a global suppression for the `CA2007` warning, which advises the use of `.ConfigureAwait`. This is appropriate for test code where configuring the await context is unnecessary. ### Cleanup of redundant `[SuppressMessage]` attributes: * Removed `[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")]` attributes from the following test classes: - `OpenFeatureClientBenchmarks` in `test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs` - `FeatureProviderTests` in `test/OpenFeature.Tests/FeatureProviderTests.cs` - `LoggingHookTests` in `test/OpenFeature.Tests/Hooks/LoggingHookTests.cs` - `OpenFeatureClientTests` in `test/OpenFeature.Tests/OpenFeatureClientTests.cs` - `OpenFeatureEventTest` in `test/OpenFeature.Tests/OpenFeatureEventTests.cs` - `OpenFeatureHookTests` in `test/OpenFeature.Tests/OpenFeatureHookTests.cs` - `OpenFeatureTests` in `test/OpenFeature.Tests/OpenFeatureTests.cs` - `ProviderRepositoryTests` in `test/OpenFeature.Tests/ProviderRepositoryTests.cs` - `InMemoryProviderTests` in `test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs` - `TestUtilsTest` in `test/OpenFeature.Tests/TestUtilsTest.cs` ### Removal of unused imports: * Removed `System.Diagnostics.CodeAnalysis` using directives from files where `[SuppressMessage]` attributes were deleted. Examples include: - `test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs` - `test/OpenFeature.Tests/FeatureProviderTests.cs` - `test/OpenFeature.Tests/Hooks/LoggingHookTests.cs` These changes simplify the codebase by centralizing the suppression of `ConfigureAwait` warnings and removing unnecessary annotations and imports. --------- Signed-off-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- build/Common.tests.props | 5 +++++ test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs | 4 +--- test/OpenFeature.Tests/FeatureProviderTests.cs | 2 -- test/OpenFeature.Tests/Hooks/LoggingHookTests.cs | 3 --- test/OpenFeature.Tests/OpenFeatureClientTests.cs | 2 -- test/OpenFeature.Tests/OpenFeatureEventTests.cs | 2 -- test/OpenFeature.Tests/OpenFeatureHookTests.cs | 2 -- test/OpenFeature.Tests/OpenFeatureTests.cs | 2 -- test/OpenFeature.Tests/ProviderRepositoryTests.cs | 2 -- .../Providers/Memory/InMemoryProviderTests.cs | 2 -- test/OpenFeature.Tests/TestUtilsTest.cs | 2 -- 11 files changed, 6 insertions(+), 22 deletions(-) diff --git a/build/Common.tests.props b/build/Common.tests.props index 8ea5c27d..ac9a6453 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -18,4 +18,9 @@ + + + + $(NoWarn);CA2007 + diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index c2779a31..d4c770eb 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using AutoFixture; using BenchmarkDotNet.Attributes; @@ -10,10 +9,9 @@ namespace OpenFeature.Benchmark; [MemoryDiagnoser] -[SimpleJob(RuntimeMoniker.Net60, baseline: true)] +[SimpleJob(RuntimeMoniker.Net80, baseline: true)] [JsonExporterAttribute.Full] [JsonExporterAttribute.FullCompressed] -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureClientBenchmarks { private readonly string _domain; diff --git a/test/OpenFeature.Tests/FeatureProviderTests.cs b/test/OpenFeature.Tests/FeatureProviderTests.cs index 79ab2f00..d7e5ca22 100644 --- a/test/OpenFeature.Tests/FeatureProviderTests.cs +++ b/test/OpenFeature.Tests/FeatureProviderTests.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using AutoFixture; using NSubstitute; @@ -9,7 +8,6 @@ namespace OpenFeature.Tests; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class FeatureProviderTests : ClearOpenFeatureInstanceFixture { [Fact] diff --git a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs index 461455eb..1364f83f 100644 --- a/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/LoggingHookTests.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -10,8 +9,6 @@ namespace OpenFeature.Tests.Hooks; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] - public class LoggingHookTests { [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 31450a6f..a43cf4d8 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -18,7 +17,6 @@ namespace OpenFeature.Tests; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture { [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index c8cea92b..d47a530f 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using AutoFixture; @@ -12,7 +11,6 @@ namespace OpenFeature.Tests; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureEventTest : ClearOpenFeatureInstanceFixture { [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index d2e9b5e9..cebe40c0 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,7 +15,6 @@ namespace OpenFeature.Tests; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture { [Fact] diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 4dea7f39..6afcc91c 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using NSubstitute; @@ -10,7 +9,6 @@ namespace OpenFeature.Tests; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureTests : ClearOpenFeatureInstanceFixture { [Fact] diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index 046d750a..16fb5d13 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using NSubstitute; using OpenFeature.Constant; @@ -11,7 +10,6 @@ namespace OpenFeature.Tests; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class ProviderRepositoryTests { [Fact] diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 8f1520a7..6a196fd5 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Error; @@ -9,7 +8,6 @@ namespace OpenFeature.Tests.Providers.Memory; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class InMemoryProviderTests { private FeatureProvider commonProvider; diff --git a/test/OpenFeature.Tests/TestUtilsTest.cs b/test/OpenFeature.Tests/TestUtilsTest.cs index 9f5cde86..ab7867ba 100644 --- a/test/OpenFeature.Tests/TestUtilsTest.cs +++ b/test/OpenFeature.Tests/TestUtilsTest.cs @@ -1,11 +1,9 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Xunit; namespace OpenFeature.Tests; -[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class TestUtilsTest { [Fact] From 9b04485173978d600a4e3fd24df111347070dc70 Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Tue, 6 May 2025 18:00:47 +0100 Subject: [PATCH 314/316] feat: Add Extension Method for adding global Hook via DependencyInjection (#459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR - This adds a new OpenFeatureBuilder extension method to add global or not domain-bound hooks to the OpenFeature Api. ### Related Issues Fixes #456 ### Notes We inject any provided Hooks as Singletons in the DI container. We use keyed singletons and use the class name as the key. Maybe we'd want to use a different name to avoid conflicts? I've done some manual testing with a sample weatherforecast ASP.NET Core web application ### Follow-up Tasks ### How to test --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> Co-authored-by: AndrΓ© Silva <2493377+askpt@users.noreply.github.com> --- README.md | 2 + .../Internal/FeatureLifecycleManager.cs | 9 +++ .../OpenFeatureBuilderExtensions.cs | 41 ++++++++++++ .../OpenFeatureOptions.cs | 12 ++++ .../FeatureLifecycleManagerTests.cs | 65 ++++++++++++------- .../NoOpHook.cs | 26 ++++++++ .../OpenFeatureBuilderExtensionsTests.cs | 60 +++++++++++++++++ .../FeatureFlagIntegrationTest.cs | 38 ++++++++++- .../OpenFeature.IntegrationTests.csproj | 1 + 9 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpHook.cs diff --git a/README.md b/README.md index 3e51a690..b0063d3a 100644 --- a/README.md +++ b/README.md @@ -434,6 +434,7 @@ builder.Services.AddOpenFeature(featureBuilder => { featureBuilder .AddHostedFeatureLifecycle() // From Hosting package .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ }) + .AddHook() .AddInMemoryProvider(); }); ``` @@ -446,6 +447,7 @@ builder.Services.AddOpenFeature(featureBuilder => { featureBuilder .AddHostedFeatureLifecycle() .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ }) + .AddHook((serviceProvider) => new LoggingHook( /* Custom configuration */ )) .AddInMemoryProvider("name1") .AddInMemoryProvider("name2") .AddPolicyName(options => { diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index d14d421b..f2c914f2 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -34,6 +34,15 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke var featureProvider = _serviceProvider.GetRequiredKeyedService(name); await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false); } + + var hooks = new List(); + foreach (var hookName in options.HookNames) + { + var hook = _serviceProvider.GetRequiredKeyedService(hookName); + hooks.Add(hook); + } + + _featureApi.AddHooks(hooks); } /// diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index a9c3f258..8f79f394 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -262,4 +262,45 @@ public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder /// The configured instance. public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) => AddPolicyName(builder, configureOptions); + + /// + /// Adds a feature hook to the service collection using a factory method. Hooks added here are not domain-bound. + /// + /// The type of to be added. + /// The instance. + /// Optional factory for controlling how will be created in the DI container. + /// The instance. + public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, Func? implementationFactory = null) + where THook : Hook + { + return builder.AddHook(typeof(THook).Name, implementationFactory); + } + + /// + /// Adds a feature hook to the service collection using a factory method and specified name. Hooks added here are not domain-bound. + /// + /// The type of to be added. + /// The instance. + /// The name of the that is being added. + /// Optional factory for controlling how will be created in the DI container. + /// The instance. + public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, string hookName, Func? implementationFactory = null) + where THook : Hook + { + builder.Services.PostConfigure(options => options.AddHookName(hookName)); + + if (implementationFactory is not null) + { + builder.Services.TryAddKeyedSingleton(hookName, (serviceProvider, key) => + { + return implementationFactory(serviceProvider); + }); + } + else + { + builder.Services.TryAddKeyedSingleton(hookName); + } + + return builder; + } } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs index b2f15e44..e9cc3cb1 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -46,4 +46,16 @@ protected internal void AddProviderName(string? name) } } } + + private readonly HashSet _hookNames = []; + + internal IReadOnlyCollection HookNames => _hookNames; + + internal void AddHookName(string name) + { + lock (_hookNames) + { + _hookNames.Add(name); + } + } } diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index c5573604..47cc7df5 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NSubstitute; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging.Abstractions; using OpenFeature.DependencyInjection.Internal; using Xunit; @@ -9,27 +8,18 @@ namespace OpenFeature.DependencyInjection.Tests; public class FeatureLifecycleManagerTests { - private readonly FeatureLifecycleManager _systemUnderTest; - private readonly IServiceProvider _mockServiceProvider; + private readonly IServiceCollection _serviceCollection; public FeatureLifecycleManagerTests() { Api.Instance.SetContext(null); Api.Instance.ClearHooks(); - _mockServiceProvider = Substitute.For(); - - var options = new OpenFeatureOptions(); - options.AddDefaultProviderName(); - var optionsMock = Substitute.For>(); - optionsMock.Value.Returns(options); - - _mockServiceProvider.GetService>().Returns(optionsMock); - - _systemUnderTest = new FeatureLifecycleManager( - Api.Instance, - _mockServiceProvider, - Substitute.For>()); + _serviceCollection = new ServiceCollection() + .Configure(options => + { + options.AddDefaultProviderName(); + }); } [Fact] @@ -37,10 +27,13 @@ public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExi { // Arrange var featureProvider = new NoOpFeatureProvider(); - _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(featureProvider); + _serviceCollection.AddSingleton(featureProvider); + + var serviceProvider = _serviceCollection.BuildServiceProvider(); + var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger.Instance); // Act - await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(true); + await sut.EnsureInitializedAsync().ConfigureAwait(true); // Assert Assert.Equal(featureProvider, Api.Instance.GetProvider()); @@ -50,14 +43,42 @@ public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExi public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist() { // Arrange - _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); + _serviceCollection.RemoveAll(); + + var serviceProvider = _serviceCollection.BuildServiceProvider(); + var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger.Instance); // Act - var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask(); + var act = () => sut.EnsureInitializedAsync().AsTask(); // Assert var exception = await Assert.ThrowsAsync(act).ConfigureAwait(true); Assert.NotNull(exception); Assert.False(string.IsNullOrWhiteSpace(exception.Message)); } + + [Fact] + public async Task EnsureInitializedAsync_ShouldSetHook_WhenHooksAreRegistered() + { + // Arrange + var featureProvider = new NoOpFeatureProvider(); + var hook = new NoOpHook(); + + _serviceCollection.AddSingleton(featureProvider) + .AddKeyedSingleton("NoOpHook", (_, key) => hook) + .Configure(options => + { + options.AddHookName("NoOpHook"); + }); + + var serviceProvider = _serviceCollection.BuildServiceProvider(); + var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger.Instance); + + // Act + await sut.EnsureInitializedAsync().ConfigureAwait(true); + + // Assert + var actual = Api.Instance.GetHooks().FirstOrDefault(); + Assert.Equal(hook, actual); + } } diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpHook.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpHook.cs new file mode 100644 index 00000000..cee6ef1d --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpHook.cs @@ -0,0 +1,26 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +internal class NoOpHook : Hook +{ + public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + return base.BeforeAsync(context, hints, cancellationToken); + } + + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + return base.AfterAsync(context, details, hints, cancellationToken); + } + + public override ValueTask FinallyAsync(HookContext context, FlagEvaluationDetails evaluationDetails, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken); + } + + public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + return base.ErrorAsync(context, error, hints, cancellationToken); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 6985125d..07597703 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -241,4 +241,64 @@ public void AddProvider_ConfiguresPolicyNameAcrossMultipleProviderSetups(int pro Assert.NotNull(provider); Assert.IsType(provider); } + + [Fact] + public void AddHook_AddsHookAsKeyedService() + { + // Arrange + _systemUnderTest.AddHook(); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var hook = serviceProvider.GetKeyedService("NoOpHook"); + + // Assert + Assert.NotNull(hook); + } + + [Fact] + public void AddHook_AddsHookNameToOpenFeatureOptions() + { + // Arrange + _systemUnderTest.AddHook(sp => new NoOpHook()); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>(); + + // Assert + Assert.Contains(options.Value.HookNames, t => t == "NoOpHook"); + } + + [Fact] + public void AddHook_WithSpecifiedNameToOpenFeatureOptions() + { + // Arrange + _systemUnderTest.AddHook("my-custom-name"); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var hook = serviceProvider.GetKeyedService("my-custom-name"); + + // Assert + Assert.NotNull(hook); + } + + [Fact] + public void AddHook_WithSpecifiedNameAndImplementationFactory_AsKeyedService() + { + // Arrange + _systemUnderTest.AddHook("my-custom-name", (serviceProvider) => new NoOpHook()); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var hook = serviceProvider.GetKeyedService("my-custom-name"); + + // Assert + Assert.NotNull(hook); + } } diff --git a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs index 2f9746eb..9e1f4bca 100644 --- a/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs +++ b/test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs @@ -5,7 +5,9 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging.Testing; using OpenFeature.DependencyInjection.Providers.Memory; +using OpenFeature.Hooks; using OpenFeature.IntegrationTests.Services; using OpenFeature.Providers.Memory; @@ -27,7 +29,8 @@ public class FeatureFlagIntegrationTest public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string userId, bool expectedResult, ServiceLifetime serviceLifetime) { // Arrange - using var server = await CreateServerAsync(serviceLifetime, services => + var logger = new FakeLogger(); + using var server = await CreateServerAsync(serviceLifetime, logger, services => { switch (serviceLifetime) { @@ -50,7 +53,7 @@ public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string us // Act var response = await client.GetAsync(requestUri).ConfigureAwait(true); - var responseContent = await response.Content.ReadFromJsonAsync>().ConfigureAwait(true); ; + var responseContent = await response.Content.ReadFromJsonAsync>().ConfigureAwait(true); // Assert Assert.True(response.IsSuccessStatusCode, "Expected HTTP status code 200 OK."); @@ -59,7 +62,35 @@ public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string us Assert.Equal(expectedResult, responseContent.FeatureValue); } - private static async Task CreateServerAsync(ServiceLifetime serviceLifetime, Action? configureServices = null) + [Fact] + public async Task VerifyLoggingHookIsRegisteredAsync() + { + // Arrange + var logger = new FakeLogger(); + using var server = await CreateServerAsync(ServiceLifetime.Transient, logger, services => + { + services.AddTransient(); + }).ConfigureAwait(true); + + var client = server.CreateClient(); + var requestUri = $"/features/{TestUserId}/flags/{FeatureA}"; + + // Act + var response = await client.GetAsync(requestUri).ConfigureAwait(true); + var logs = logger.Collector.GetSnapshot(); + + // Assert + Assert.True(response.IsSuccessStatusCode, "Expected HTTP status code 200 OK."); + Assert.Equal(4, logs.Count); + Assert.Multiple(() => + { + Assert.Contains("Before Flag Evaluation", logs[0].Message); + Assert.Contains("After Flag Evaluation", logs[1].Message); + }); + } + + private static async Task CreateServerAsync(ServiceLifetime serviceLifetime, FakeLogger logger, + Action? configureServices = null) { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); @@ -94,6 +125,7 @@ private static async Task CreateServerAsync(ServiceLifetime serviceL return flagService.GetFlags(); } }); + cfg.AddHook(serviceProvider => new LoggingHook(logger)); }); var app = builder.Build(); diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj index baf5fdfb..151c61b9 100644 --- a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj +++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj @@ -13,6 +13,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive From ea76351a095b7eeb777941aaf7ac42e4d925c366 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 May 2025 22:31:29 +0100 Subject: [PATCH 315/316] chore(deps): update github/codeql-action digest to 60168ef (#463) 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://redirect.github.com/github/codeql-action) | action | digest | `28deaed` -> `60168ef` | --- ### 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 d943ef01..50c33905 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@28deaeda66b76a05916b6923827895f2b14ab387 # v3 + uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # 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@28deaeda66b76a05916b6923827895f2b14ab387 # v3 + uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # 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@28deaeda66b76a05916b6923827895f2b14ab387 # v3 + uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 From 0a5ab0c3c71a1a615b0ee8627dd4ff5db39cac9b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 May 2025 21:32:09 +0000 Subject: [PATCH 316/316] chore(deps): update actions/attest-build-provenance action to v2.3.0 (#464) 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 | |---|---|---|---| | [actions/attest-build-provenance](https://redirect.github.com/actions/attest-build-provenance) | action | minor | `v2.2.3` -> `v2.3.0` | --- ### Release Notes
actions/attest-build-provenance (actions/attest-build-provenance) ### [`v2.3.0`](https://redirect.github.com/actions/attest-build-provenance/releases/tag/v2.3.0) [Compare Source](https://redirect.github.com/actions/attest-build-provenance/compare/v2.2.3...v2.3.0) #### What's Changed - Bump `actions/attest` from 2.2.1 to 2.3.0 by [@​bdehamer](https://redirect.github.com/bdehamer) in [https://github.com/actions/attest-build-provenance/pull/615](https://redirect.github.com/actions/attest-build-provenance/pull/615) - Updates `@sigstore/oci` from 0.4.0 to 0.5.0 **Full Changelog**: https://github.com/actions/attest-build-provenance/compare/v2.2.3...v2.3.0
--- ### 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/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3702d88b..339c5c8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,7 @@ jobs: run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.NUGET_TOKEN }}" --source https://api.nuget.org/v3/index.json - name: Generate artifact attestation - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 with: subject-path: "src/**/*.nupkg"