Skip to content

Commit

Permalink
feat: Add ConfigCat provider (open-feature#119)
Browse files Browse the repository at this point in the history
Signed-off-by: Luiz Bon <luiz.bon@prospa.com>
  • Loading branch information
luizbon authored Jan 16, 2024
1 parent 0f9dd90 commit 20aeb3a
Show file tree
Hide file tree
Showing 11 changed files with 531 additions and 2 deletions.
4 changes: 4 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ components:
src/OpenFeature.Contrib.Providers.Flagsmith:
- vpetrusevici
- matthewelwell
src/OpenFeature.Contrib.Providers.ConfigCat:
- luizbon

# test/
test/OpenFeature.Contrib.Hooks.Otel.Test:
Expand All @@ -27,6 +29,8 @@ components:
test/OpenFeature.Contrib.Providers.Flagsmith.Test:
- vpetrusevici
- matthewelwell
test/OpenFeature.Contrib.Providers.ConfigCat.Test:
- luizbon

ignored-authors:
- renovate-bot
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.3",
"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.Providers.ConfigCat", "src\OpenFeature.Contrib.Providers.ConfigCat\OpenFeature.Contrib.Providers.ConfigCat.csproj", "{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.ConfigCat.Test", "test\OpenFeature.Contrib.Providers.ConfigCat.Test\OpenFeature.Contrib.Providers.ConfigCat.Test.csproj", "{047835AC-A8E3-432A-942D-0BDC1E9FC3BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.FeatureManagement", "src\OpenFeature.Contrib.Providers.FeatureManagement\OpenFeature.Contrib.Providers.FeatureManagement.csproj", "{2F988A3F-727F-4326-995D-9C123A5E44AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.FeatureManagement.Test", "test\OpenFeature.Contrib.Providers.FeatureManagement.Test\OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj", "{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB}"
Expand Down Expand Up @@ -65,6 +69,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
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -86,6 +98,8 @@ 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}
{2F988A3F-727F-4326-995D-9C123A5E44AA} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
EndGlobalSection
Expand Down
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.Providers.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);
}

/// <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)
{
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.Providers.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.Providers.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.Providers.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" };

internal static User BuildUser(this EvaluationContext context)
{
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;
}
}

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

0 comments on commit 20aeb3a

Please sign in to comment.