Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
514798a
feat(Unleash): add Unleash provider
PSanetra May 19, 2026
3b4c7f0
feat(Unleash): use file bootstrap for tests, emit ConfigurationChange…
PSanetra May 19, 2026
5a2b943
feat(Unleash): expose payload-type as flag metadata in resolution det…
PSanetra May 19, 2026
9bca632
ci: add Unleash provider to release-please configuration
PSanetra May 19, 2026
b2a625b
docs(Unleash): add version.txt and README.md
PSanetra May 19, 2026
1de0631
test(Unleash): remove workaround
PSanetra May 19, 2026
ee1618b
refactor(Unleash): rename OpenFeature.Contrib.Providers.Unleash to Op…
PSanetra May 19, 2026
a9f8674
Merge branch 'main' into feat/add-unleash-provider
lukas-reining May 19, 2026
5f833a6
refactor(Unleash): address PR review feedback and improve initializat…
PSanetra May 20, 2026
a39700d
fix(Unleash): return ResolutionDetails with TypeMismatch error instea…
PSanetra May 20, 2026
a5cad57
fix(Unleash): return Reason.Disabled when variant is DISABLED_VARIANT
PSanetra May 20, 2026
72bbfe7
refactor(Unleash): remove baseline context storage and merge logic
PSanetra May 21, 2026
8d3b727
docs(Unleash): add README.md to OpenFeature.Providers.Unleash.csproj
PSanetra May 22, 2026
a69c1b6
chore(Unleash): add psanetra as component owner
PSanetra May 22, 2026
f90a1db
fix(Unleash): clear client reference on shutdown to prevent use-after…
PSanetra May 26, 2026
0ab267a
feat(Unleash): honor cancellation tokens in resolve and shutdown methods
PSanetra May 26, 2026
9765d96
Merge remote-tracking branch 'origin/main' into feat/add-unleash-prov…
PSanetra May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ components:
src/OpenFeature.Providers.Ofrep:
- askpt
- weyert
src/OpenFeature.Providers.Unleash:
- PSanetra

# test/
test/OpenFeature.Providers.Flagd.Test:
Expand Down Expand Up @@ -55,6 +57,8 @@ components:
test/OpenFeature.Providers.Ofrep.Test:
- askpt
- weyert
test/OpenFeature.Providers.Unleash.Test:
- PSanetra

ignored-authors:
- renovate-bot
5 changes: 3 additions & 2 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"src/OpenFeature.Contrib.Providers.EnvVar": "0.0.7",
"src/OpenFeature.Providers.Flagd": "0.6.1",
"src/OpenFeature.Providers.Ofrep": "0.1.5",
"src/OpenFeature.Providers.GOFeatureFlag": "1.0.0"
}
"src/OpenFeature.Providers.GOFeatureFlag": "1.0.0",
"src/OpenFeature.Providers.Unleash": "0.1.0"
}
2 changes: 2 additions & 0 deletions DotnetSdkContrib.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Project Path="src/OpenFeature.Contrib.Providers.Statsig/OpenFeature.Contrib.Providers.Statsig.csproj" />
<Project Path="src/OpenFeature.Providers.Ofrep/OpenFeature.Providers.Ofrep.csproj" />
<Project Path="src/OpenFeature.Providers.GOFeatureFlag/OpenFeature.Providers.GOFeatureFlag.csproj" />
<Project Path="src\OpenFeature.Providers.Unleash\OpenFeature.Providers.Unleash.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test/OpenFeature.Contrib.Providers.ConfigCat.Test/OpenFeature.Contrib.Providers.ConfigCat.Test.csproj" />
Expand All @@ -23,5 +24,6 @@
<Project Path="test/OpenFeature.Contrib.Providers.Statsig.Test/OpenFeature.Contrib.Providers.Statsig.Test.csproj" />
<Project Path="test/OpenFeature.Providers.Ofrep.Test/OpenFeature.Providers.Ofrep.Test.csproj" />
<Project Path="test/OpenFeature.Providers.GOFeatureFlag.Test/OpenFeature.Providers.GOFeatureFlag.Test.csproj" />
<Project Path="test\OpenFeature.Providers.Unleash.Test\OpenFeature.Providers.Unleash.Test.csproj" />
</Folder>
</Solution>
10 changes: 10 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@
"extra-files": [
"OpenFeature.Providers.Ofrep.csproj"
]
},
"src/OpenFeature.Providers.Unleash": {
"package-name": "OpenFeature.Providers.Unleash",
"release-type": "simple",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"versioning": "default",
"extra-files": [
"OpenFeature.Providers.Unleash.csproj"
]
}
},
"changelog-sections": [
Expand Down
106 changes: 106 additions & 0 deletions src/OpenFeature.Providers.Unleash/EvaluationContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using OpenFeature.Model;
using Unleash;

namespace OpenFeature.Providers.Unleash;

/// <summary>
/// Extension methods for transforming an OpenFeature EvaluationContext into an Unleash UnleashContext.
/// </summary>
internal static class EvaluationContextExtensions
{
private const string UserIdKey = "userId";
private const string SessionIdKey = "sessionId";
private const string RemoteAddressKey = "remoteAddress";
private const string EnvironmentKey = "environment";
private const string AppNameKey = "appName";
private const string CurrentTimeKey = "currentTime";

/// <summary>
/// Gets the appName value from the evaluation context, or null if not present.
/// </summary>
/// <param name="context">The evaluation context.</param>
/// <returns>The appName value, or null.</returns>
public static string? GetAppName(this EvaluationContext? context)
{
if (context == null)
{
return null;
}

return context.TryGetValue(AppNameKey, out var value) ? value?.AsString : null;
}

/// <summary>
/// Transforms an OpenFeature EvaluationContext into an Unleash UnleashContext.
/// </summary>
/// <param name="context">The evaluation context, may be null.</param>
/// <returns>A new UnleashContext populated from the context.</returns>
public static UnleashContext ToUnleashContext(this EvaluationContext? context)
{
if (context == null)
{
return new UnleashContext();
}

string? userId = null;
string? sessionId = null;
string? remoteAddress = null;
string? environment = null;
string? appName = null;
DateTimeOffset? currentTime = null;
var properties = new Dictionary<string, string>();

if (!string.IsNullOrWhiteSpace(context.TargetingKey))
{
userId = context.TargetingKey;
}

foreach (var kvp in context)
{
var key = kvp.Key;
var value = kvp.Value;

if (value == null)
{
continue;
}

var stringValue = value.AsString;

switch (key)
{
case UserIdKey:
userId = stringValue;
break;
case SessionIdKey:
sessionId = stringValue;
break;
case RemoteAddressKey:
remoteAddress = stringValue;
break;
case EnvironmentKey:
environment = stringValue;
break;
case AppNameKey:
appName = stringValue;
break;
case CurrentTimeKey:
if (DateTimeOffset.TryParse(stringValue, out var parsed))
{
currentTime = parsed;
}
break;
default:
if (stringValue != null)
{
properties[key] = stringValue;
}
break;
}
}

return new UnleashContext(appName, environment, userId, sessionId, remoteAddress, currentTime, properties);
}
}
Comment thread
askpt marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>OpenFeature.Providers.Unleash</PackageId>
<VersionNumber>0.1.0</VersionNumber> <!--x-release-please-version -->
<VersionPrefix>$(VersionNumber)</VersionPrefix>
<AssemblyVersion>$(VersionNumber)</AssemblyVersion>
<FileVersion>$(VersionNumber)</FileVersion>
<Description>Unleash provider for OpenFeature .NET SDK</Description>
<Authors>OpenFeature Authors</Authors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RootNamespace>OpenFeature.Providers.Unleash</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>

<!-- documentation -->
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<!-- make the internal methods visible to our test project -->
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(MSBuildProjectName).Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Unleash.Client" Version="[6.2.1,7.0)" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="OpenFeature" Version="[2.0,3.0)" />
</ItemGroup>

</Project>
112 changes: 112 additions & 0 deletions src/OpenFeature.Providers.Unleash/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Unleash .NET Provider

The Unleash provider allows you to use [Unleash](https://www.getunleash.io/) with the OpenFeature .NET SDK.

# .Net SDK usage

## Requirements

- open-feature/dotnet-sdk v2.x
- Unleash .NET SDK v6.x

## Install dependencies

The first thing we will do is install the **OpenFeature SDK** and the **Unleash Feature Flag provider**.

### .NET Cli

```shell
dotnet add package OpenFeature.Providers.Unleash
```

### Package Manager

```shell
NuGet\Install-Package OpenFeature.Providers.Unleash
```

### Package Reference

```xml
<PackageReference Include="OpenFeature.Providers.Unleash" />
```

### Packet cli

```shell
paket add OpenFeature.Providers.Unleash
```

### Cake

```shell
// Install OpenFeature.Providers.Unleash as a Cake Addin
#addin nuget:?package=OpenFeature.Providers.Unleash

// Install OpenFeature.Providers.Unleash as a Cake Tool
#tool nuget:?package=OpenFeature.Providers.Unleash
```

## Using the Unleash Provider with the OpenFeature SDK

```csharp
using OpenFeature;
using OpenFeature.Providers.Unleash;
using Unleash;

var settings = new UnleashSettings
{
AppName = "my-app",
UnleashApi = new Uri("http://localhost:4242/api/"),
CustomHttpHeaders = new Dictionary<string, string>
{
{ "Authorization", "*:development.your-api-token" }
}
};

var provider = new UnleashProvider(settings);

// Set the provider for the OpenFeature SDK
await Api.Instance.SetProviderAsync(provider);

// Get an OpenFeature client
var client = Api.Instance.GetClient();

// Boolean evaluation (uses IsEnabled)
var enabled = await client.GetBooleanValueAsync("my-feature", false);

// String evaluation (uses variant payload)
var value = await client.GetStringValueAsync("my-variant-flag", "default");

// Integer evaluation (parses variant payload)
var count = await client.GetIntegerValueAsync("my-int-flag", 0);

// Double evaluation (parses variant payload)
var rate = await client.GetDoubleValueAsync("my-double-flag", 0.0);
```

## EvaluationContext and Unleash Context relationship

The provider maps OpenFeature `EvaluationContext` fields to `UnleashContext`:

| EvaluationContext Key | Unleash Context Field |
|-----------------------|------------------------|
| `TargetingKey` | `UserId` |
| `sessionId` | `SessionId` |
| `remoteAddress` | `RemoteAddress` |
| `environment` | `Environment` |
| `appName` | `AppName` |
| `currentTime` | `CurrentTime` |
| All other keys | `Properties` |

## Variant payload type metadata

When evaluating variants (string, integer, double, structure), the provider exposes the Unleash payload `type` field (e.g., `"string"`, `"number"`, `"json"`, `"csv"`) as `payload-type` in the resolution details flag metadata.

## Events

The provider emits `ProviderConfigurationChanged` events when Unleash fires `TogglesUpdatedEvent` (i.e., when toggle state is refreshed from the server).

## Known issues and limitations

- The provider does not accept an external `IUnleash` instance because lifecycle events (`ReadyEvent`, `ErrorEvent`, `TogglesUpdatedEvent`) can only be subscribed during client construction.
Loading