From 8931538d537d0fe93f7ffd15d733083db6baa703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:50:16 +0100 Subject: [PATCH 1/8] refactor: remove FlagEvaluationError logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/OpenFeatureClient.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 8c39621b..f7b6bc3a 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -290,7 +290,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 +396,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); From 0528d283df8e87736a0ee7a93d138a4058a0fb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:51:27 +0100 Subject: [PATCH 2/8] docs: improve README formatting and clarity 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 | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index fb6550bd..c9cf7e0a 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. #### 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). + From e33037f0acc43d3cdda0bffd4fa3b9fd9536a910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:54:44 +0100 Subject: [PATCH 3/8] refactor: improve error handling in ResolveIntegerValueAsync method 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/Providers/Memory/InMemoryProvider.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 4a06dc85..c22a9874 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. From 1bea1a12a22f88d54904c15c3dd0dde0042ee694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:56:08 +0100 Subject: [PATCH 4/8] fix: correct string interpolation in TypeMismatchException message 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/Providers/Memory/InMemoryProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index c22a9874..f95a0a06 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -112,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)}"); } } } From 02b1d1fb271d037da87865c7e61fd35222650314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:43:45 +0100 Subject: [PATCH 5/8] refactor: update MissingFlag test to return FlagNotFound evaluation flag and improve assertions 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> --- test/OpenFeature.Tests/OpenFeatureClientTests.cs | 2 +- .../Providers/Memory/InMemoryProviderTests.cs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) 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 21ed4defda66c851da9a10a6bb4cfc653b230f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sun, 6 Apr 2025 16:55:00 +0100 Subject: [PATCH 6/8] docs: clarify logging behavior and mention Logging Hook in README 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 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9cf7e0a..2ba47096 100644 --- a/README.md +++ b/README.md @@ -154,7 +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. +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 From c9e5ded5a36b3130b2743c75f408ffbfa5842edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sun, 6 Apr 2025 17:02:15 +0100 Subject: [PATCH 7/8] fix: remove redundant error handling call in FeatureProviderException MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/OpenFeatureClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index f7b6bc3a..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); } From 07dd69f8b6384b0f33505439aa054f79b9d30280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sun, 6 Apr 2025 17:18:21 +0100 Subject: [PATCH 8/8] fix: update rollForward strategy in global.json to latestFeature 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> --- global.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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