diff --git a/README.md b/README.md
index fb6550bd8..2ba470960 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
+

## .NET SDK
@@ -9,13 +10,14 @@
[](https://github.com/open-feature/spec/releases/tag/v0.7.0)
[
- 
+
](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.3.2)
[](https://cloud-native.slack.com/archives/C0344AANLA1)
[](https://codecov.io/gh/open-feature/dotnet-sdk)
[](https://www.nuget.org/packages/OpenFeature)
[](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
[](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 46506cda5..3018f657c 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 8c39621b4..03420a2a4 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 4a06dc85d..f95a0a06a 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 c16824cb7..fc6f415f4 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 c83ce0ce5..7a174fc51 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);