Skip to content

Commit

Permalink
fix: Revise ConfigCat provider (#280)
Browse files Browse the repository at this point in the history
Signed-off-by: Adam Simon <[email protected]>
  • Loading branch information
adams85 authored Sep 17, 2024
1 parent 829b591 commit 0b2d5f2
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 63 deletions.
13 changes: 13 additions & 0 deletions src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ public ConfigCatProvider(string sdkKey, Action<ConfigCatClientOptions> configBui
Client = ConfigCatClient.Get(sdkKey, configBuilder);
}

/// <inheritdoc/>
public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)
{
return Client.WaitForReadyAsync(cancellationToken);
}

/// <inheritdoc/>
public override Task ShutdownAsync(CancellationToken cancellationToken = default)
{
Client.Dispose();
return Task.CompletedTask;
}

/// <inheritdoc/>
public override Metadata GetMetadata()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="ConfigCat.Client" Version="9.2.0"/>
<PackageReference Include="ConfigCat.Client" Version="9.3.1"/>
</ItemGroup>
</Project>
90 changes: 47 additions & 43 deletions src/OpenFeature.Contrib.Providers.ConfigCat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

# .Net SDK usage
# .NET SDK usage

## Requirements

Expand Down Expand Up @@ -47,68 +47,72 @@ paket add OpenFeature.Contrib.Providers.ConfigCat
The following example shows how to use the ConfigCat provider with the OpenFeature SDK.

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

namespace OpenFeatureTestApp
var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#");

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

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

var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false);
if (isAwesomeFeatureEnabled)
{
doTheNewThing();
}
else
{
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.GetBooleanValueAsync("isMyAwesomeFeatureEnabled", false);

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

### Customizing the ConfigCat Provider

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

```csharp
var configCatOptions = new ConfigCatClientOptions
Action<ConfigCat.Client.Configuration.ConfigCatClientOptions> configureConfigCatOptions = (options) =>
{
PollingMode = PollingModes.ManualPoll;
Logger = new ConsoleLogger(LogLevel.Info);
options.PollingMode = PollingModes.LazyLoad(cacheTimeToLive: TimeSpan.FromSeconds(10));
options.Logger = new ConsoleLogger(LogLevel.Info);
// ...
};

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

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

## EvaluationContext and ConfigCat User relationship
### Cleaning up

On application shutdown, clean up the OpenFeature provider and the underlying ConfigCat client.

```csharp
await OpenFeature.Api.Instance.ShutdownAsync();
```

## EvaluationContext and ConfigCat User Object 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.
An <a href="https://openfeature.dev/docs/reference/concepts/evaluation-context" target="_blank">evaluation context</a> in the OpenFeature specification is a container for arbitrary contextual data that can be used as a basis for feature flag evaluation.
The ConfigCat provider translates these evaluation contexts to ConfigCat [User Objects](https://configcat.com/docs/targeting/user-object/).

The ConfigCat User has a few pre-defined parameters that can be used to evaluate a flag. These are:
The ConfigCat User Object has a few pre-defined attributes 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. |
| Attribute | Description |
|--------------|----------------------------------------------------------------------------------------------------------------|
| `Identifier` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. |
| `Email` | The email address of the user. |
| `Country` | The country of the user. |

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.
Since `EvaluationContext` is a simple dictionary, the provider will try to match the keys to ConfigCat user attributes following the table below in a case-insensitive manner.

| EvaluationContext Key | ConfigCat User Parameter |
| EvaluationContext Key | ConfigCat User Attribute |
|-----------------------|--------------------------|
| `id` | `Id` |
| `identifier` | `Id` |
| `id` | `Identifier` |
| `identifier` | `Identifier` |
| `email` | `Email` |
| `country` | `Country` |
| `country` | `Country` |
| Any other | `Custom` |
32 changes: 14 additions & 18 deletions src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ConfigCat.Client;
using OpenFeature.Model;
Expand All @@ -17,35 +16,32 @@ internal static User BuildUser(this EvaluationContext context)
return null;
}

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

foreach (var value in context)
{
switch (value.Key.ToUpperInvariant())
if (StringComparer.OrdinalIgnoreCase.Equals("EMAIL", value.Key))
{
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;
user.Email = value.Value.AsString;
}
else if (StringComparer.OrdinalIgnoreCase.Equals("COUNTRY", value.Key))
{
user.Country = value.Value.AsString;
}
else
{
user.Custom.Add(value.Key, value.Value.AsString);
}
}

return user;
}

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

return pair.Key != null;
return pair.Key != null ? pair.Value.AsString : "<n/a>";
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture.Xunit2;
using ConfigCat.Client;
Expand All @@ -12,14 +14,58 @@ namespace OpenFeature.Contrib.ConfigCat.Test
{
public class ConfigCatProviderTest
{
const string TestConfigJson =
@"
{
""f"": {
""isAwesomeFeatureEnabled"": {
""t"": 0,
""v"": {
""b"": true
}
},
""isPOCFeatureEnabled"": {
""t"": 0,
""r"": [
{
""c"": [
{
""u"": {
""a"": ""Email"",
""c"": 2,
""l"": [
""@example.com""
]
}
}
],
""s"": {
""v"": {
""b"": true
}
}
}
],
""v"": {
""b"": false
}
}
}
}
";

[Theory]
[AutoData]
public void CreateConfigCatProvider_WithSdkKey_CreatesProviderInstanceSuccessfully(string sdkKey)
public async void CreateConfigCatProvider_WithSdkKey_CreatesProviderInstanceSuccessfully(string sdkKey)
{
var configCatProvider =
new ConfigCatProvider(sdkKey, options => { options.FlagOverrides = BuildFlagOverrides(); });

await configCatProvider.InitializeAsync(EvaluationContext.Empty);

Assert.NotNull(configCatProvider.Client);

await configCatProvider.ShutdownAsync();
}

[Theory]
Expand Down Expand Up @@ -93,33 +139,70 @@ public async Task GetStructureValueAsync_ForFeature_ReturnExpectedResult(string
var configCatProvider = new ConfigCatProvider(sdkKey,
options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", defaultValue.AsString)); });

await configCatProvider.InitializeAsync(EvaluationContext.Empty);

var result = await configCatProvider.ResolveStructureValueAsync("example-feature", defaultValue);

Assert.Equal(defaultValue.AsString, result.Value.AsString);
Assert.Equal("example-feature", result.FlagKey);
Assert.Equal(ErrorType.None, result.ErrorType);

await configCatProvider.ShutdownAsync();
}

[Theory]
[InlineAutoData("[email protected]", false)]
[InlineAutoData("[email protected]", true)]
public async Task OpenFeatureAPI_EndToEnd_Test(string email, bool expectedValue)
{
var configCatProvider = new ConfigCatProvider("fake-67890123456789012/1234567890123456789012", options =>
{ options.ConfigFetcher = new FakeConfigFetcher(TestConfigJson); });

await OpenFeature.Api.Instance.SetProviderAsync(configCatProvider);

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

var evaluationContext = EvaluationContext.Builder()
.Set("email", email)
.Build();

var result = await client.GetBooleanDetailsAsync("isPOCFeatureEnabled", false, evaluationContext);

Assert.Equal(expectedValue, result.Value);
Assert.Equal("isPOCFeatureEnabled", result.FlagKey);
Assert.Equal(ErrorType.None, result.ErrorType);

await OpenFeature.Api.Instance.ShutdownAsync();
}

private static async Task ExecuteResolveTest<T>(object value, T defaultValue, T expectedValue, string sdkKey, Func<ConfigCatProvider, string, T, Task<ResolutionDetails<T>>> resolveFunc)
{
var configCatProvider = new ConfigCatProvider(sdkKey,
options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", value)); });

await configCatProvider.InitializeAsync(EvaluationContext.Empty);

var result = await resolveFunc(configCatProvider, "example-feature", defaultValue);

Assert.Equal(expectedValue, result.Value);
Assert.Equal("example-feature", result.FlagKey);
Assert.Equal(ErrorType.None, result.ErrorType);

await configCatProvider.ShutdownAsync();
}

private static async Task ExecuteResolveErrorTest<T>(object value, T defaultValue, ErrorType expectedErrorType, string sdkKey, Func<ConfigCatProvider, string, T, Task<ResolutionDetails<T>>> resolveFunc)
{
var configCatProvider = new ConfigCatProvider(sdkKey,
options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", value)); });

await configCatProvider.InitializeAsync(EvaluationContext.Empty);

var exception = await Assert.ThrowsAsync<FeatureProviderException>(() => resolveFunc(configCatProvider, "example-feature", defaultValue));

Assert.Equal(expectedErrorType, exception.ErrorType);

await configCatProvider.ShutdownAsync();
}

private static FlagOverrides BuildFlagOverrides(params (string key, object value)[] values)
Expand All @@ -132,5 +215,22 @@ private static FlagOverrides BuildFlagOverrides(params (string key, object value

return FlagOverrides.LocalDictionary(dictionary, OverrideBehaviour.LocalOnly);
}

private sealed class FakeConfigFetcher : IConfigCatConfigFetcher
{
private readonly string configJson;

public FakeConfigFetcher(string configJson)
{
this.configJson = configJson;
}

public void Dispose() { }

public Task<FetchResponse> FetchAsync(FetchRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(new FetchResponse(HttpStatusCode.OK, reasonPhrase: null, headers: Array.Empty<KeyValuePair<string, string>>(), this.configJson));
}
}
}
}

0 comments on commit 0b2d5f2

Please sign in to comment.