Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ConfigCat provider #119

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2b187ad
chore(deps): update dependency grpc.net.client to v2.59.0 (#115)
renovate[bot] Dec 19, 2023
2230d67
Add ConfigCat provider
luizbon Dec 20, 2023
2150d2d
Add message to unsupported feature
luizbon Dec 20, 2023
53505d8
Implement Stucture with JSON string
luizbon Dec 21, 2023
69dc9ee
Refactor ConfigCatProvider and UserBuilder classes
luizbon Dec 21, 2023
5ce4344
Add ConfigCat provider
luizbon Dec 20, 2023
b76a5a8
Add message to unsupported feature
luizbon Dec 20, 2023
254a50b
Implement Stucture with JSON string
luizbon Dec 21, 2023
26d00cc
Refactor ConfigCatProvider and UserBuilder classes
luizbon Dec 21, 2023
eddd56f
chore(deps): update dotnet monorepo to v8 (major) (#100)
renovate[bot] Dec 21, 2023
7b1a329
fix: do not send targeting key as separate trait in flagsmith (#120)
vpetrusevici Dec 21, 2023
da99c30
chore(main): release OpenFeature.Contrib.Providers.Flagsmith 0.1.5 (#…
github-actions[bot] Dec 21, 2023
77cf631
chore(deps): update dependency grpc.tools to v2.60.0 (#111)
renovate[bot] Dec 21, 2023
0f8e06d
Merge branch 'main' into feat/add-configcat-provider
luizbon Dec 21, 2023
9ccf8c7
dotnet format fix
luizbon Dec 21, 2023
ce31dda
Merge remote-tracking branch 'upstream/main' into feat/add-configcat-…
luizbon Jan 3, 2024
eafc48c
Address PR comments
luizbon Jan 4, 2024
cf64290
Remove IDisposable implementation
luizbon Jan 4, 2024
852d9a0
Merge branch 'main' into feat/add-configcat-provider
luizbon Jan 9, 2024
38c2202
Apply dotnet-format
luizbon Jan 10, 2024
4b8699f
Merge branch 'main' into feat/add-configcat-provider
luizbon Jan 10, 2024
1c73522
Add myself to component owners
luizbon Jan 15, 2024
e570072
Merge remote-tracking branch 'upstream/main' into feat/add-configcat-…
luizbon Jan 15, 2024
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
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"src/OpenFeature.Contrib.Hooks.Otel": "0.1.2",
"src/OpenFeature.Contrib.Providers.Flagd": "0.1.7",
"src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.1.5",
"src/OpenFeature.Contrib.Providers.Flagsmith": "0.1.5"
"src/OpenFeature.Contrib.Providers.Flagsmith": "0.1.5",
"src/OpenFeature.Contrib.Providers.ConfigCat": "0.0.1"
}
14 changes: 14 additions & 0 deletions DotnetSdkContrib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Provide
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagsmith.Test", "test\OpenFeature.Contrib.Providers.Flagsmith.Test\OpenFeature.Contrib.Providers.Flagsmith.Test.csproj", "{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.ConfigCat", "src\OpenFeature.Contrib.ConfigCat\OpenFeature.Contrib.ConfigCat.csproj", "{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.ConfigCat.Test", "test\OpenFeature.Contrib.ConfigCat.Test\OpenFeature.Contrib.ConfigCat.Test.csproj", "{047835AC-A8E3-432A-942D-0BDC1E9FC3BC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -61,6 +65,14 @@ Global
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.Build.0 = Release|Any CPU
{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Release|Any CPU.Build.0 = Release|Any CPU
{047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -74,5 +86,7 @@ Global
{4041B63F-9CF6-4886-8FC7-BD1A7E45F859} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{47008BEE-7888-4B9B-8884-712A922C3F9B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{047835AC-A8E3-432A-942D-0BDC1E9FC3BC} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
EndGlobalSection
EndGlobal
12 changes: 11 additions & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
"extra-files": [
"OpenFeature.Contrib.Providers.Flagsmith.csproj"
]
},
"src/OpenFeature.Contrib.Providers.ConfigCat": {
"package-name": "OpenFeature.Contrib.Providers.ConfigCat",
"release-type": "simple",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"versioning": "default",
"extra-files": [
"OpenFeature.Contrib.Providers.ConfigCat.csproj"
]
}
},
"changelog-sections": [
Expand Down Expand Up @@ -98,4 +108,4 @@
}
],
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}
}
110 changes: 110 additions & 0 deletions src/OpenFeature.Contrib.ConfigCat/ConfigCatProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.Threading.Tasks;
using ConfigCat.Client;
using ConfigCat.Client.Configuration;
using OpenFeature.Constant;
using OpenFeature.Error;
using OpenFeature.Model;

namespace OpenFeature.Contrib.ConfigCat
{
/// <summary>
/// ConfigCatProvider is the .NET provider implementation for the feature flag solution ConfigCat.
/// </summary>
public sealed class ConfigCatProvider : FeatureProvider
{
private const string Name = "ConfigCat Provider";
internal readonly IConfigCatClient Client;

/// <summary>
/// Creates new instance of <see cref="ConfigCatProvider"/>
/// </summary>
/// <param name="sdkKey">SDK Key to access the ConfigCat config.</param>
/// <param name="configBuilder">The action used to configure the client.</param>
/// <exception cref="ArgumentNullException"><paramref name="sdkKey"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="sdkKey"/> is an empty string or in an invalid format.</exception>
public ConfigCatProvider(string sdkKey, Action<ConfigCatClientOptions> configBuilder = null)
{
Client = ConfigCatClient.Get(sdkKey, configBuilder);
luizbon marked this conversation as resolved.
Show resolved Hide resolved
}

/// <inheritdoc/>
public override Metadata GetMetadata()
{
return new Metadata(Name);
}

/// <inheritdoc/>
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null)
{
return ResolveFlag(flagKey, context, defaultValue);
}

/// <inheritdoc/>
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null)
{
return ResolveFlag(flagKey, context, defaultValue);
}

/// <inheritdoc/>
public override Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null)
{
return ResolveFlag(flagKey, context, defaultValue);
}

/// <inheritdoc/>
public override Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null)
{
return ResolveFlag(flagKey, context, defaultValue);
}

/// <inheritdoc/>
public override async Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null)
{
var user = context?.BuildUser();
var result = await Client.GetValueDetailsAsync(flagKey, defaultValue?.AsObject, user);
var returnValue = result.IsDefaultValue ? defaultValue : new Value(result.Value);
var details = new ResolutionDetails<Value>(flagKey, returnValue, ParseErrorType(result.ErrorMessage), errorMessage: result.ErrorMessage, variant: result.VariationId);
if(details.ErrorType == ErrorType.None)
{
return details;
}

throw new FeatureProviderException(details.ErrorType, details.ErrorMessage);
}

private async Task<ResolutionDetails<T>> ResolveFlag<T>(string flagKey, EvaluationContext context, T defaultValue)
{
var user = context?.BuildUser();
var result = await Client.GetValueDetailsAsync(flagKey, defaultValue, user);
var details = new ResolutionDetails<T>(flagKey, result.Value, ParseErrorType(result.ErrorMessage), errorMessage: result.ErrorMessage, variant: result.VariationId);
if(details.ErrorType == ErrorType.None)
{
return details;
}

throw new FeatureProviderException(details.ErrorType, details.ErrorMessage);
}

private static ErrorType ParseErrorType(string errorMessage)
luizbon marked this conversation as resolved.
Show resolved Hide resolved
{
if (string.IsNullOrEmpty(errorMessage))
{
return ErrorType.None;
}
if (errorMessage.Contains("Config JSON is not present"))
{
return ErrorType.ParseError;
}
if (errorMessage.Contains("the key was not found in config JSON"))
{
return ErrorType.FlagNotFound;
}
if (errorMessage.Contains("The type of a setting must match the type of the specified default value"))
{
return ErrorType.TypeMismatch;
}
return ErrorType.General;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>OpenFeature.Contrib.ConfigCat</PackageId>
<VersionNumber>0.0.1</VersionNumber> <!--x-release-please-version -->
<Version>$(VersionNumber)</Version>
<AssemblyVersion>$(VersionNumber)</AssemblyVersion>
<FileVersion>$(VersionNumber)</FileVersion>
<Description>ConfigCat provider for .NET</Description>
<PackageProjectUrl>https://openfeature.dev</PackageProjectUrl>
<RepositoryUrl>https://github.com/open-feature/dotnet-sdk-contrib</RepositoryUrl>
<Authors>Luiz Bon</Authors>
</PropertyGroup>
<ItemGroup>
<!-- make the internal methods visble to our test project -->
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(MSBuildProjectName).Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="ConfigCat.Client" Version="[9,)"/>
</ItemGroup>
</Project>
110 changes: 110 additions & 0 deletions src/OpenFeature.Contrib.ConfigCat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# ConfigCat Feature Flag .NET Provider

The ConfigCat Flag provider allows you to connect to your ConfigCat instance.

# .Net SDK usage

## Install dependencies

The first things we will do is install the **Open Feature SDK** and the **ConfigCat Feature Flag provider**.

### .NET Cli
```shell
dotnet add package OpenFeature.Contrib.Providers.ConfigCat
```
### Package Manager

```shell
NuGet\Install-Package OpenFeature.Contrib.Providers.ConfigCat
```
### Package Reference

```xml
<PackageReference Include="OpenFeature.Contrib.Providers.ConfigCat" />
```
### Packet cli

```shell
paket add OpenFeature.Contrib.Providers.ConfigCat
```

### Cake

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

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

## Using the ConfigCat Provider with the OpenFeature SDK

The following example shows how to use the ConfigCat provider with the OpenFeature SDK.

```csharp
using OpenFeature.Contrib.Providers.ConfigCat;

namespace OpenFeatureTestApp
{
class Hello {
static void Main(string[] args) {
var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#");

// Set the configCatProvider as the provider for the OpenFeature SDK
OpenFeature.Api.Instance.SetProvider(configCatProvider);

var client = OpenFeature.Api.Instance.GetClient();

var val = client.GetBooleanValue("isMyAwesomeFeatureEnabled", false);

if(isMyAwesomeFeatureEnabled)
{
doTheNewThing();
}
else
{
doTheOldThing();
}
}
}
}
```

### Customizing the ConfigCat Provider

The ConfigCat provider can be customized by passing a `ConfigCatClientOptions` object to the constructor.

```csharp
var configCatOptions = new ConfigCatClientOptions
{
PollingMode = PollingModes.ManualPoll;
Logger = new ConsoleLogger(LogLevel.Info);
};

var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configCatOptions);
```

For a full list of options see the [ConfigCat documentation](https://configcat.com/docs/sdk-reference/dotnet/).

## EvaluationContext and ConfigCat User relationship

ConfigCat has the concept of Users where you can evaluate a flag based on properties. The OpenFeature SDK has the concept of an EvaluationContext which is a dictionary of string keys and values. The ConfigCat provider will map the EvaluationContext to a ConfigCat User.

The ConfigCat User has a few pre-defined parameters that can be used to evaluate a flag. These are:

| Parameter | Description |
|-----------|---------------------------------------------------------------------------------------------------------------------------------|
| `Id` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. |
| `Email` | Optional parameter for easier targeting rule definitions. |
| `Country` | Optional parameter for easier targeting rule definitions. |
| `Custom` | Optional dictionary for custom attributes of a user for advanced targeting rule definitions. E.g. User role, Subscription type. |

Since EvaluationContext is a simple dictionary, the provider will try to match the keys to the ConfigCat User parameters following the table below in a case-insensitive manner.

| EvaluationContext Key | ConfigCat User Parameter |
|-----------------------|--------------------------|
| `id` | `Id` |
| `identifier` | `Id` |
| `email` | `Email` |
| `country` | `Country` |
51 changes: 51 additions & 0 deletions src/OpenFeature.Contrib.ConfigCat/UserBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ConfigCat.Client;
using OpenFeature.Model;

namespace OpenFeature.Contrib.ConfigCat
{
internal static class UserBuilder
{
private static readonly string[] PossibleUserIds = { "ID", "IDENTIFIER" };
luizbon marked this conversation as resolved.
Show resolved Hide resolved

internal static User BuildUser(this EvaluationContext context)
luizbon marked this conversation as resolved.
Show resolved Hide resolved
{
if (context == null)
{
return null;
}

var user = context.TryGetValuesInsensitive(PossibleUserIds, out var pair)
? new User(pair.Value.AsString)
: new User(Guid.NewGuid().ToString());

foreach (var value in context)
{
switch (value.Key.ToUpperInvariant())
{
case "EMAIL":
user.Email = value.Value.AsString;
continue;
case "COUNTRY":
user.Country = value.Value.AsString;
continue;
default:
user.Custom.Add(value.Key, value.Value.AsString);
continue;
}
luizbon marked this conversation as resolved.
Show resolved Hide resolved
}

return user;
}

private static bool TryGetValuesInsensitive(this EvaluationContext context, string[] keys,
out KeyValuePair<string, Value> pair)
{
pair = context.AsDictionary().FirstOrDefault(x => keys.Contains(x.Key.ToUpperInvariant()));

return pair.Key != null;
}
}
}
Loading
Loading