Skip to content

feat(Unleash): add Unleash provider#652

Open
PSanetra wants to merge 17 commits into
open-feature:mainfrom
PSanetra:feat/add-unleash-provider
Open

feat(Unleash): add Unleash provider#652
PSanetra wants to merge 17 commits into
open-feature:mainfrom
PSanetra:feat/add-unleash-provider

Conversation

@PSanetra
Copy link
Copy Markdown

@PSanetra PSanetra commented May 19, 2026

This PR

  • Adds a new OpenFeature provider for Unleash using the Unleash .NET SDK v6 and OpenFeature SDK v2
  • The provider creates and owns its own DefaultUnleash client instance (no external IUnleash accepted) to properly participate in the OpenFeature initialization lifecycle via Unleash's ReadyEvent/ErrorEvent callbacks
  • Supports file bootstrap for offline usage (ToggleBootstrapProvider), skipping HTTP initialization when configured
  • Emits ProviderConfigurationChanged events when Unleash fires TogglesUpdatedEvent
  • Maps OpenFeature EvaluationContext to Unleash context (TargetingKeyUserId, well-known keys like sessionId, remoteAddress, environment, appName, currentTime, and custom properties)
  • Boolean evaluation uses IsEnabled; string/int/double/structure use GetVariant with payload parsing
  • Exposes the Unleash variant payload-type as flag metadata in ResolutionDetails, matching the Java provider pattern

Related Issues

None

Notes

  • The provider does not accept an external IUnleash instance 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.
  • The variant payload Type field (e.g., "string", "number", "json", "csv") is exposed as payload-type flag 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).
  • Tests use file bootstrap

@PSanetra PSanetra requested review from a team as code owners May 19, 2026 08:59
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/OpenFeature.Providers.Unleash/UnleashProvider.cs
Comment thread src/OpenFeature.Providers.Unleash/UnleashProvider.cs
Comment thread src/OpenFeature.Providers.Unleash/UnleashProvider.cs
Comment thread src/OpenFeature.Providers.Unleash/UnleashProvider.cs
PSanetra added 5 commits May 19, 2026 11:21
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>
@PSanetra PSanetra force-pushed the feat/add-unleash-provider branch from 7665972 to b2a625b Compare May 19, 2026 09:21
@askpt
Copy link
Copy Markdown
Member

askpt commented May 19, 2026

@PSanetra Thank you so much for your contribution. Please note the package name should be OpenFeature.Providers.Unleash. Would you mind doing the renaming to fix it?

@PSanetra
Copy link
Copy Markdown
Author

@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 OpenFeature.Contrib.Providers.Flipt and the Flagd provider is called OpenFeature.Providers.Flagd. Is there a reason for the different names?

Unleash/unleash-dotnet-sdk#141 was fixed in Unleash.Client 6.2.1

Signed-off-by: PSanetra <code@psanetra.de>
@PSanetra PSanetra force-pushed the feat/add-unleash-provider branch from 8f1620d to 1de0631 Compare May 19, 2026 15:36
@askpt
Copy link
Copy Markdown
Member

askpt commented May 19, 2026

@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 OpenFeature.Contrib.Providers.Flipt and the Flagd provider is called OpenFeature.Providers.Flagd. Is there a reason for the different names?

@PSanetra We decided to go OpenFeature.Providers.* have a look at this issue: #154 or #195. I am trying to rename a few to follow the latest pattern but it takes time. The new providers should follow the latest guidelines.

@PSanetra PSanetra force-pushed the feat/add-unleash-provider branch from a05abb9 to 1de0631 Compare May 19, 2026 16:04
…enFeature.Providers.Unleash

Signed-off-by: PSanetra <code@psanetra.de>
@PSanetra
Copy link
Copy Markdown
Author

@askpt Alright, I have renamed the packages and namespaces 🙂

Copy link
Copy Markdown
Member

@askpt askpt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a quick assessment and it seems a very good quality PR. Thanks for your contribution!

Added some small comments.

Comment thread src/OpenFeature.Providers.Unleash/ContextTransformer.cs Outdated
Comment thread src/OpenFeature.Providers.Unleash/UnleashProvider.cs Outdated
Comment thread src/OpenFeature.Providers.Unleash/ContextTransformer.cs Outdated
string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default)
{
var unleashContext = ContextTransformer.Transform(context);
var result = this._unleash.IsEnabled(flagKey, unleashContext, defaultValue);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/OpenFeature.Providers.Unleash/UnleashProvider.cs Outdated
@askpt askpt requested a review from arttonoyan May 19, 2026 18:00
PSanetra added 3 commits May 20, 2026 14:19
…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>
@PSanetra
Copy link
Copy Markdown
Author

@askpt I think your comments can now be resolved, but please have a second look.

PSanetra added 2 commits May 21, 2026 11:38
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>
@askpt
Copy link
Copy Markdown
Member

askpt commented May 22, 2026

@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 😄

@PSanetra
Copy link
Copy Markdown
Author

No worries, I have added myself 🙂

Signed-off-by: PSanetra <code@psanetra.de>
@PSanetra PSanetra force-pushed the feat/add-unleash-provider branch from f716f07 to a69c1b6 Compare May 22, 2026 11:08
{
var client = await clientTask.ConfigureAwait(false);
client?.Dispose();
}
Copy link
Copy Markdown
Contributor

@kylejuliandev kylejuliandev May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

await provider.InitializeAsync(EvaluationContext.Empty, cts.Token);

// Evaluations still work (client was created, just not ready)
var result = await provider.ResolveBooleanValueAsync("flag", true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kylejuliandev good catch! I have added WaitAsync(cancellationToken) to those places where the client is awaited.

PSanetra added 3 commits May 26, 2026 16:47
…-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>
@PSanetra PSanetra requested review from askpt and kylejuliandev May 29, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants