forked from open-feature/dotnet-sdk-contrib
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add ConfigCat provider (open-feature#119)
Signed-off-by: Luiz Bon <[email protected]>
- Loading branch information
Showing
11 changed files
with
531 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
51
src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.