feat(Unleash): add Unleash provider#652
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new Unleash provider for the OpenFeature .NET SDK, including a context transformer to map evaluation contexts to Unleash contexts and a provider implementation for resolving various flag types. The feedback focuses on aligning the implementation with the PR description by capturing and exposing the Unleash variant payload-type as metadata within the resolution details. This involves updating the internal VariantResolution struct and the evaluation methods to ensure the payload type is correctly propagated.
Signed-off-by: PSanetra <code@psanetra.de>
…d events - Remove external IUnleash constructor; only UnleashSettings is supported since lifecycle events are only available during client construction - Skip waiting for ReadyEvent when ToggleBootstrapProvider is configured (bootstrap loads synchronously) - Emit ProviderConfigurationChanged when Unleash fires TogglesUpdatedEvent - Rewrite tests to use real DefaultUnleash with file bootstrap - Split test classes into separate files Signed-off-by: PSanetra <code@psanetra.de>
…ails Match the Java provider pattern: include the Unleash variant payload type as 'payload-type' in ImmutableMetadata on all variant-based evaluations. Also fix bootstrap.json to use 'number' type for numeric payloads per Unleash conventions. Signed-off-by: PSanetra <code@psanetra.de>
Signed-off-by: PSanetra <code@psanetra.de>
Signed-off-by: PSanetra <code@psanetra.de>
7665972 to
b2a625b
Compare
|
@PSanetra Thank you so much for your contribution. Please note the package name should be |
|
@askpt I can do that, but it seems like there is already some inconsistency between the provider package names. E.g. the Flipt provider is called |
Unleash/unleash-dotnet-sdk#141 was fixed in Unleash.Client 6.2.1 Signed-off-by: PSanetra <code@psanetra.de>
8f1620d to
1de0631
Compare
@PSanetra We decided to go |
a05abb9 to
1de0631
Compare
…enFeature.Providers.Unleash Signed-off-by: PSanetra <code@psanetra.de>
|
@askpt Alright, I have renamed the packages and namespaces 🙂 |
askpt
left a comment
There was a problem hiding this comment.
Did a quick assessment and it seems a very good quality PR. Thanks for your contribution!
Added some small comments.
| string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) | ||
| { | ||
| var unleashContext = ContextTransformer.Transform(context); | ||
| var result = this._unleash.IsEnabled(flagKey, unleashContext, defaultValue); |
There was a problem hiding this comment.
I am not familiar with Unleash but, does it return a Evaluation Details? I am asking because you are assuming all with be static evaluations and in case flag_not_found for example, we should populate the ResolutionDetails correctly.
This applies to all evaluations.
There was a problem hiding this comment.
If I have not missed something, I think it is not easily possible to distinguish between disabled and non existing flags. In the non-boolean cases I now return Reason.Disabled if the unleash.GetVariant(...) call returns Variant.DISABLED_VARIANT. But I think this is also returned when the flag does not exist.
I think in theory we could also check if the flag is returned from unleash.ListKnownToggles(). But I am not sure if it really returns a flag if it is disabled.
…ion lifecycle - Rename ContextTransformer to EvaluationContextExtensions with extension method context.ToUnleashContext() - Add GetAppName() extension method for safe appName extraction - Use string.IsNullOrWhiteSpace for TargetingKey check - Enable nullable reference types on both projects - Emit PROVIDER_ERROR event when Unleash fires ErrorEvent - Replace direct IUnleash field with TaskCompletionSource<IUnleash>: InitializeAsync awaits the TCS which resolves eventually once ReadyEvent, ErrorEvent, or cancellation occurs. The client instance is always provided as the result (Unleash SDK handles its own defaults when not yet ready). - Evaluation methods return ProviderNotReady only when InitializeAsync has not been called at all (_clientTcs is null) - ShutdownAsync awaits the client task before disposing - Store initialization EvaluationContext as baseline and merge it with per-call context using EvaluationContextBuilder.Merge (per-call wins) - Apply appName from initialization context to UnleashSettings - Add tests for: EmitProviderError, ProviderNotReady before init, whitespace targeting key, cancellation, error completion, SetReady Signed-off-by: PSanetra <code@psanetra.de>
…d of throwing Replace TypeMismatchException throws with ResolutionDetails containing ErrorType.TypeMismatch and Reason.Error when variant payloads cannot be parsed as the requested numeric type. This returns the default value along with error metadata rather than raising an exception. Signed-off-by: PSanetra <code@psanetra.de>
When GetVariant returns DISABLED_VARIANT (toggle disabled or not found), return ResolutionDetails with Reason.Disabled instead of Reason.Default. This more accurately reflects the Unleash SDK semantics. Signed-off-by: PSanetra <code@psanetra.de>
|
@askpt I think your comments can now be resolved, but please have a second look. |
OpenFeature SDK already merges the initialization context with per-call context before passing it to the provider, so storing and merging it ourselves is redundant. Signed-off-by: PSanetra <code@psanetra.de>
Signed-off-by: PSanetra <code@psanetra.de>
|
@PSanetra Could you please add yourself as a component owner? https://github.com/open-feature/dotnet-sdk-contrib/blob/main/.github/component_owners.yml Sorry if I am slowly reviewing this PR. The initialisation and lifecycle are quite complex 😄 |
|
No worries, I have added myself 🙂 |
Signed-off-by: PSanetra <code@psanetra.de>
f716f07 to
a69c1b6
Compare
| { | ||
| var client = await clientTask.ConfigureAwait(false); | ||
| client?.Dispose(); | ||
| } |
There was a problem hiding this comment.
suggestion(non-blocking): should the field this._clientTcs be set to null after we have disposed of it? This would prevent subsequent calls to resolve a flag from throwing a Disposed exception, and instead get a ProviderNotReady error
| await provider.InitializeAsync(EvaluationContext.Empty, cts.Token); | ||
|
|
||
| // Evaluations still work (client was created, just not ready) | ||
| var result = await provider.ResolveBooleanValueAsync("flag", true); |
There was a problem hiding this comment.
thought: looking at the method I don't think the CancellationToken is passed to the Unleash task & client, so if a user needed to cancel during flag evaluation this wouldn't be possible?
There was a problem hiding this comment.
@kylejuliandev good catch! I have added WaitAsync(cancellationToken) to those places where the client is awaited.
…-dispose Nulling _clientTcs in ShutdownAsync ensures post-shutdown evaluations return ProviderNotReady instead of calling into the disposed client. Signed-off-by: PSanetra <code@psanetra.de>
Use WaitAsync(cancellationToken) so awaiting the client task can be cancelled. Also target net8.0;net10.0 for both src and test projects. Signed-off-by: PSanetra <code@psanetra.de>
This PR
DefaultUnleashclient instance (no externalIUnleashaccepted) to properly participate in the OpenFeature initialization lifecycle via Unleash'sReadyEvent/ErrorEventcallbacksToggleBootstrapProvider), skipping HTTP initialization when configuredProviderConfigurationChangedevents when Unleash firesTogglesUpdatedEventEvaluationContextto Unleash context (TargetingKey→UserId, well-known keys likesessionId,remoteAddress,environment,appName,currentTime, and custom properties)IsEnabled; string/int/double/structure useGetVariantwith payload parsingpayload-typeas flag metadata inResolutionDetails, matching the Java provider patternRelated Issues
None
Notes
IUnleashinstance because the Unleash SDK only allows subscribing to lifecycle events (ReadyEvent,ErrorEvent,TogglesUpdatedEvent) during client construction. Without these events, the provider cannot properly signal readiness or configuration changes to the OpenFeature SDK.Typefield (e.g.,"string","number","json","csv") is exposed aspayload-typeflag metadata for consumers to inspect. The provider does not alter parsing behavior based on the type — it always treats the value as a raw string and attempts parsing based on the OpenFeature evaluation method called (matching the Java provider behavior).