Skip to content

Commit 0b2d5f2

Browse files
authored
fix: Revise ConfigCat provider (#280)
Signed-off-by: Adam Simon <[email protected]>
1 parent 829b591 commit 0b2d5f2

File tree

5 files changed

+176
-63
lines changed

5 files changed

+176
-63
lines changed

src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs

+13
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ public ConfigCatProvider(string sdkKey, Action<ConfigCatClientOptions> configBui
2929
Client = ConfigCatClient.Get(sdkKey, configBuilder);
3030
}
3131

32+
/// <inheritdoc/>
33+
public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)
34+
{
35+
return Client.WaitForReadyAsync(cancellationToken);
36+
}
37+
38+
/// <inheritdoc/>
39+
public override Task ShutdownAsync(CancellationToken cancellationToken = default)
40+
{
41+
Client.Dispose();
42+
return Task.CompletedTask;
43+
}
44+
3245
/// <inheritdoc/>
3346
public override Metadata GetMetadata()
3447
{

src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
</AssemblyAttribute>
1717
</ItemGroup>
1818
<ItemGroup>
19-
<PackageReference Include="ConfigCat.Client" Version="9.2.0"/>
19+
<PackageReference Include="ConfigCat.Client" Version="9.3.1"/>
2020
</ItemGroup>
2121
</Project>

src/OpenFeature.Contrib.Providers.ConfigCat/README.md

+47-43
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

5-
# .Net SDK usage
5+
# .NET SDK usage
66

77
## Requirements
88

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

4949
```csharp
50-
using OpenFeature.Contrib.Providers.ConfigCat;
50+
using System;
51+
using ConfigCat.Client;
52+
using OpenFeature.Contrib.ConfigCat;
5153

52-
namespace OpenFeatureTestApp
54+
var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#");
55+
56+
// Set the configCatProvider as the provider for the OpenFeature SDK
57+
await OpenFeature.Api.Instance.SetProviderAsync(configCatProvider);
58+
59+
var client = OpenFeature.Api.Instance.GetClient();
60+
61+
var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false);
62+
if (isAwesomeFeatureEnabled)
63+
{
64+
doTheNewThing();
65+
}
66+
else
5367
{
54-
class Hello {
55-
static void Main(string[] args) {
56-
var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#");
57-
58-
// Set the configCatProvider as the provider for the OpenFeature SDK
59-
OpenFeature.Api.Instance.SetProvider(configCatProvider);
60-
61-
var client = OpenFeature.Api.Instance.GetClient();
62-
63-
var val = client.GetBooleanValueAsync("isMyAwesomeFeatureEnabled", false);
64-
65-
if(isMyAwesomeFeatureEnabled)
66-
{
67-
doTheNewThing();
68-
}
69-
else
70-
{
71-
doTheOldThing();
72-
}
73-
}
74-
}
68+
doTheOldThing();
7569
}
7670
```
7771

7872
### Customizing the ConfigCat Provider
7973

80-
The ConfigCat provider can be customized by passing a `ConfigCatClientOptions` object to the constructor.
74+
The ConfigCat provider can be customized by passing a callback setting up a `ConfigCatClientOptions` object to the constructor.
8175

8276
```csharp
83-
var configCatOptions = new ConfigCatClientOptions
77+
Action<ConfigCat.Client.Configuration.ConfigCatClientOptions> configureConfigCatOptions = (options) =>
8478
{
85-
PollingMode = PollingModes.ManualPoll;
86-
Logger = new ConsoleLogger(LogLevel.Info);
79+
options.PollingMode = PollingModes.LazyLoad(cacheTimeToLive: TimeSpan.FromSeconds(10));
80+
options.Logger = new ConsoleLogger(LogLevel.Info);
81+
// ...
8782
};
8883

89-
var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configCatOptions);
84+
var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configureConfigCatOptions);
9085
```
9186

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

94-
## EvaluationContext and ConfigCat User relationship
89+
### Cleaning up
90+
91+
On application shutdown, clean up the OpenFeature provider and the underlying ConfigCat client.
92+
93+
```csharp
94+
await OpenFeature.Api.Instance.ShutdownAsync();
95+
```
96+
97+
## EvaluationContext and ConfigCat User Object relationship
9598

96-
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.
99+
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.
100+
The ConfigCat provider translates these evaluation contexts to ConfigCat [User Objects](https://configcat.com/docs/targeting/user-object/).
97101

98-
The ConfigCat User has a few pre-defined parameters that can be used to evaluate a flag. These are:
102+
The ConfigCat User Object has a few pre-defined attributes that can be used to evaluate a flag. These are:
99103

100-
| Parameter | Description |
101-
|-----------|---------------------------------------------------------------------------------------------------------------------------------|
102-
| `Id` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. |
103-
| `Email` | Optional parameter for easier targeting rule definitions. |
104-
| `Country` | Optional parameter for easier targeting rule definitions. |
105-
| `Custom` | Optional dictionary for custom attributes of a user for advanced targeting rule definitions. E.g. User role, Subscription type. |
104+
| Attribute | Description |
105+
|--------------|----------------------------------------------------------------------------------------------------------------|
106+
| `Identifier` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. |
107+
| `Email` | The email address of the user. |
108+
| `Country` | The country of the user. |
106109

107-
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.
110+
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.
108111

109-
| EvaluationContext Key | ConfigCat User Parameter |
112+
| EvaluationContext Key | ConfigCat User Attribute |
110113
|-----------------------|--------------------------|
111-
| `id` | `Id` |
112-
| `identifier` | `Id` |
114+
| `id` | `Identifier` |
115+
| `identifier` | `Identifier` |
113116
| `email` | `Email` |
114-
| `country` | `Country` |
117+
| `country` | `Country` |
118+
| Any other | `Custom` |
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Linq;
43
using ConfigCat.Client;
54
using OpenFeature.Model;
@@ -17,35 +16,32 @@ internal static User BuildUser(this EvaluationContext context)
1716
return null;
1817
}
1918

20-
var user = context.TryGetValuesInsensitive(PossibleUserIds, out var pair)
21-
? new User(pair.Value.AsString)
22-
: new User(Guid.NewGuid().ToString());
19+
var user = new User(context.GetUserId());
2320

2421
foreach (var value in context)
2522
{
26-
switch (value.Key.ToUpperInvariant())
23+
if (StringComparer.OrdinalIgnoreCase.Equals("EMAIL", value.Key))
2724
{
28-
case "EMAIL":
29-
user.Email = value.Value.AsString;
30-
continue;
31-
case "COUNTRY":
32-
user.Country = value.Value.AsString;
33-
continue;
34-
default:
35-
user.Custom.Add(value.Key, value.Value.AsString);
36-
continue;
25+
user.Email = value.Value.AsString;
26+
}
27+
else if (StringComparer.OrdinalIgnoreCase.Equals("COUNTRY", value.Key))
28+
{
29+
user.Country = value.Value.AsString;
30+
}
31+
else
32+
{
33+
user.Custom.Add(value.Key, value.Value.AsString);
3734
}
3835
}
3936

4037
return user;
4138
}
4239

43-
private static bool TryGetValuesInsensitive(this EvaluationContext context, string[] keys,
44-
out KeyValuePair<string, Value> pair)
40+
private static string GetUserId(this EvaluationContext context)
4541
{
46-
pair = context.AsDictionary().FirstOrDefault(x => keys.Contains(x.Key.ToUpperInvariant()));
42+
var pair = context.AsDictionary().FirstOrDefault(x => PossibleUserIds.Contains(x.Key, StringComparer.OrdinalIgnoreCase));
4743

48-
return pair.Key != null;
44+
return pair.Key != null ? pair.Value.AsString : "<n/a>";
4945
}
5046
}
5147
}

test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs

+101-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Net;
4+
using System.Threading;
35
using System.Threading.Tasks;
46
using AutoFixture.Xunit2;
57
using ConfigCat.Client;
@@ -12,14 +14,58 @@ namespace OpenFeature.Contrib.ConfigCat.Test
1214
{
1315
public class ConfigCatProviderTest
1416
{
17+
const string TestConfigJson =
18+
@"
19+
{
20+
""f"": {
21+
""isAwesomeFeatureEnabled"": {
22+
""t"": 0,
23+
""v"": {
24+
""b"": true
25+
}
26+
},
27+
""isPOCFeatureEnabled"": {
28+
""t"": 0,
29+
""r"": [
30+
{
31+
""c"": [
32+
{
33+
""u"": {
34+
""a"": ""Email"",
35+
""c"": 2,
36+
""l"": [
37+
""@example.com""
38+
]
39+
}
40+
}
41+
],
42+
""s"": {
43+
""v"": {
44+
""b"": true
45+
}
46+
}
47+
}
48+
],
49+
""v"": {
50+
""b"": false
51+
}
52+
}
53+
}
54+
}
55+
";
56+
1557
[Theory]
1658
[AutoData]
17-
public void CreateConfigCatProvider_WithSdkKey_CreatesProviderInstanceSuccessfully(string sdkKey)
59+
public async void CreateConfigCatProvider_WithSdkKey_CreatesProviderInstanceSuccessfully(string sdkKey)
1860
{
1961
var configCatProvider =
2062
new ConfigCatProvider(sdkKey, options => { options.FlagOverrides = BuildFlagOverrides(); });
2163

64+
await configCatProvider.InitializeAsync(EvaluationContext.Empty);
65+
2266
Assert.NotNull(configCatProvider.Client);
67+
68+
await configCatProvider.ShutdownAsync();
2369
}
2470

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

142+
await configCatProvider.InitializeAsync(EvaluationContext.Empty);
143+
96144
var result = await configCatProvider.ResolveStructureValueAsync("example-feature", defaultValue);
97145

98146
Assert.Equal(defaultValue.AsString, result.Value.AsString);
99147
Assert.Equal("example-feature", result.FlagKey);
100148
Assert.Equal(ErrorType.None, result.ErrorType);
149+
150+
await configCatProvider.ShutdownAsync();
151+
}
152+
153+
[Theory]
154+
[InlineAutoData("[email protected]", false)]
155+
[InlineAutoData("[email protected]", true)]
156+
public async Task OpenFeatureAPI_EndToEnd_Test(string email, bool expectedValue)
157+
{
158+
var configCatProvider = new ConfigCatProvider("fake-67890123456789012/1234567890123456789012", options =>
159+
{ options.ConfigFetcher = new FakeConfigFetcher(TestConfigJson); });
160+
161+
await OpenFeature.Api.Instance.SetProviderAsync(configCatProvider);
162+
163+
var client = OpenFeature.Api.Instance.GetClient();
164+
165+
var evaluationContext = EvaluationContext.Builder()
166+
.Set("email", email)
167+
.Build();
168+
169+
var result = await client.GetBooleanDetailsAsync("isPOCFeatureEnabled", false, evaluationContext);
170+
171+
Assert.Equal(expectedValue, result.Value);
172+
Assert.Equal("isPOCFeatureEnabled", result.FlagKey);
173+
Assert.Equal(ErrorType.None, result.ErrorType);
174+
175+
await OpenFeature.Api.Instance.ShutdownAsync();
101176
}
102177

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

183+
await configCatProvider.InitializeAsync(EvaluationContext.Empty);
184+
108185
var result = await resolveFunc(configCatProvider, "example-feature", defaultValue);
109186

110187
Assert.Equal(expectedValue, result.Value);
111188
Assert.Equal("example-feature", result.FlagKey);
112189
Assert.Equal(ErrorType.None, result.ErrorType);
190+
191+
await configCatProvider.ShutdownAsync();
113192
}
114193

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

199+
await configCatProvider.InitializeAsync(EvaluationContext.Empty);
200+
120201
var exception = await Assert.ThrowsAsync<FeatureProviderException>(() => resolveFunc(configCatProvider, "example-feature", defaultValue));
121202

122203
Assert.Equal(expectedErrorType, exception.ErrorType);
204+
205+
await configCatProvider.ShutdownAsync();
123206
}
124207

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

133216
return FlagOverrides.LocalDictionary(dictionary, OverrideBehaviour.LocalOnly);
134217
}
218+
219+
private sealed class FakeConfigFetcher : IConfigCatConfigFetcher
220+
{
221+
private readonly string configJson;
222+
223+
public FakeConfigFetcher(string configJson)
224+
{
225+
this.configJson = configJson;
226+
}
227+
228+
public void Dispose() { }
229+
230+
public Task<FetchResponse> FetchAsync(FetchRequest request, CancellationToken cancellationToken)
231+
{
232+
return Task.FromResult(new FetchResponse(HttpStatusCode.OK, reasonPhrase: null, headers: Array.Empty<KeyValuePair<string, string>>(), this.configJson));
233+
}
234+
}
135235
}
136236
}

0 commit comments

Comments
 (0)