From d1ca06bccc1ee28f9e7c38603caadbb80a5c844a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20H=C3=B8rup?= Date: Tue, 29 Apr 2025 23:36:11 +0200 Subject: [PATCH 1/3] Add caching to GoFeatureFlag provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Torben Hørup --- .../GoFeatureFlagProvider.cs | 100 +++++++++++++----- .../GoFeatureFlagProviderOptions.cs | 15 ++- ...ure.Contrib.Providers.GOFeatureFlag.csproj | 8 +- 3 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs index 4b22ec8a..0c459c4c 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs @@ -9,6 +9,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; using OpenFeature.Constant; using OpenFeature.Contrib.Providers.GOFeatureFlag.converters; using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; @@ -16,6 +17,7 @@ using OpenFeature.Contrib.Providers.GOFeatureFlag.hooks; using OpenFeature.Contrib.Providers.GOFeatureFlag.models; using OpenFeature.Model; +using ZiggyCreatures.Caching.Fusion; namespace OpenFeature.Contrib.Providers.GOFeatureFlag { @@ -28,6 +30,8 @@ public class GoFeatureFlagProvider : FeatureProvider private ExporterMetadata _exporterMetadata; private HttpClient _httpClient; + private IFusionCache _cache = null; + /// /// Constructor of the provider. /// Options used while creating the provider @@ -87,6 +91,27 @@ private void InitializeProvider(GoFeatureFlagProviderOptions options) if (options.ApiKey != null) _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey); + + + var cacheMaxSize = options.CacheMaxSize ?? 10000; + var cacheMaxTTL = options.CacheMaxTTL ?? TimeSpan.FromSeconds(60); + + var backingMemoryCache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = cacheMaxSize, + CompactionPercentage = 0.1, + }); + + _cache = new FusionCache(new FusionCacheOptions + { + DefaultEntryOptions = new FusionCacheEntryOptions + { + Size = 1, + Duration = cacheMaxTTL, + }, + + }, backingMemoryCache); + } /// @@ -114,9 +139,13 @@ public override async Task> ResolveBooleanValueAsync(str { try { - var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); - return new ResolutionDetails(flagKey, bool.Parse(resp.Value.ToString()), ErrorType.None, - resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + var key = GenerateCacheKey(flagKey, context); + return await _cache.GetOrSetAsync>(key, async (_, _) => + { + var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); + return new ResolutionDetails(flagKey, bool.Parse(resp.Value.ToString()), ErrorType.None, + resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + }).ConfigureAwait(false); } catch (FormatException e) { @@ -145,12 +174,16 @@ public override async Task> ResolveStringValueAsync(st EvaluationContext context = null, CancellationToken cancellationToken = default) { try - { - var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); - if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String)) - throw new TypeMismatchError($"flag value {flagKey} had unexpected type"); - return new ResolutionDetails(flagKey, resp.Value.ToString(), ErrorType.None, resp.Reason, - resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + { + var key = GenerateCacheKey(flagKey, context); + return await _cache.GetOrSetAsync>(key, async (_,_) => + { + var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); + if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String)) + throw new TypeMismatchError($"flag value {flagKey} had unexpected type"); + return new ResolutionDetails(flagKey, resp.Value.ToString(), ErrorType.None, resp.Reason, + resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + }).ConfigureAwait(false); } catch (FormatException e) { @@ -179,9 +212,13 @@ public override async Task> ResolveIntegerValueAsync(stri { try { - var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); - return new ResolutionDetails(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None, - resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + var key = GenerateCacheKey(flagKey, context); + return await _cache.GetOrSetAsync>(key, async (_,_) => + { + var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); + return new ResolutionDetails(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None, + resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + }).ConfigureAwait(false); } catch (FormatException e) { @@ -211,10 +248,14 @@ public override async Task> ResolveDoubleValueAsync(st { try { - var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); - return new ResolutionDetails(flagKey, - double.Parse(resp.Value.ToString(), CultureInfo.InvariantCulture), ErrorType.None, - resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + var key = GenerateCacheKey(flagKey, context); + return await _cache.GetOrSetAsync>(key, async (_,_) => + { + var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); + return new ResolutionDetails(flagKey, + double.Parse(resp.Value.ToString(), CultureInfo.InvariantCulture), ErrorType.None, + resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + }).ConfigureAwait(false); } catch (FormatException e) { @@ -244,15 +285,19 @@ public override async Task> ResolveStructureValueAsync( { try { - var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); - if (resp.Value is JsonElement) - { - var value = ConvertValue((JsonElement)resp.Value); - return new ResolutionDetails(flagKey, value, ErrorType.None, resp.Reason, - resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); - } - - throw new TypeMismatchError($"flag value {flagKey} had unexpected type"); + var key = GenerateCacheKey(flagKey, context); + return await _cache.GetOrSetAsync>(key, async (_,_) => + { + var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); + if (resp.Value is JsonElement) + { + var value = ConvertValue((JsonElement)resp.Value); + return new ResolutionDetails(flagKey, value, ErrorType.None, resp.Reason, + resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); + } + + throw new TypeMismatchError($"flag value {flagKey} had unexpected type"); + }).ConfigureAwait(false); } catch (FormatException e) { @@ -311,6 +356,11 @@ private async Task CallApi(string flagKey, T defaultValue, return ofrepResp; } + private string GenerateCacheKey(string flagKey, EvaluationContext ctx) + { + return ctx != null ? new OfrepRequest(ctx).AsJsonString() : flagKey; + } + /// /// convertValue is converting the object return by the proxy response in the right type. /// diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs index 3d3100cc..a293e710 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs @@ -40,6 +40,19 @@ public class GoFeatureFlagProviderOptions /// (optional) ExporterMetadata are static information you can set that will be available in the /// evaluation data sent to the exporter. /// - public ExporterMetadata ExporterMetadata { get; set; } + public ExporterMetadata ExporterMetadata { get; set; } + + + /// + /// (optional) How long is an entry allowed to be cached in memory + /// Default: 60 seconds + /// + public TimeSpan? CacheMaxTTL { get; set; } + + /// + /// (optional) How many entries may be cached + /// Default: 10000 + /// + public int? CacheMaxSize { get; set; } } } diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj index 3dc488c8..8552abc2 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj @@ -1,4 +1,4 @@ - + OpenFeature.Contrib.GOFeatureFlag @@ -15,8 +15,12 @@ + + + + - 8.0 + 9.0 From 6214c3d5a9aa59aa24ebc004a0d5ae180984416a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20H=C3=B8rup?= Date: Sat, 3 May 2025 15:47:29 +0200 Subject: [PATCH 2/3] PR comments and unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Torben Hørup --- .../GoFeatureFlagProvider.cs | 35 +++--- .../GoFeatureFlagProviderOptions.cs | 4 +- ...ure.Contrib.Providers.GOFeatureFlag.csproj | 2 +- .../GoFeatureFlagProviderTest.cs | 106 ++++++++++++++++-- 4 files changed, 120 insertions(+), 27 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs index 0c459c4c..dd151924 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Threading; @@ -19,6 +20,7 @@ using OpenFeature.Model; using ZiggyCreatures.Caching.Fusion; +[assembly: InternalsVisibleTo("OpenFeature.Contrib.Providers.GOFeatureFlag.Test")] namespace OpenFeature.Contrib.Providers.GOFeatureFlag { /// @@ -30,7 +32,8 @@ public class GoFeatureFlagProvider : FeatureProvider private ExporterMetadata _exporterMetadata; private HttpClient _httpClient; - private IFusionCache _cache = null; + internal IFusionCache _cache = null; + internal MemoryCache _backingCache = null; /// /// Constructor of the provider. @@ -93,27 +96,25 @@ private void InitializeProvider(GoFeatureFlagProviderOptions options) new AuthenticationHeaderValue("Bearer", options.ApiKey); - var cacheMaxSize = options.CacheMaxSize ?? 10000; - var cacheMaxTTL = options.CacheMaxTTL ?? TimeSpan.FromSeconds(60); - var backingMemoryCache = new MemoryCache(new MemoryCacheOptions + _backingCache = new MemoryCache(new MemoryCacheOptions { - SizeLimit = cacheMaxSize, - CompactionPercentage = 0.1, + SizeLimit = options.CacheMaxSize, + CompactionPercentage = 0.1, }); - + _cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new FusionCacheEntryOptions { Size = 1, - Duration = cacheMaxTTL, + Duration = options.CacheMaxTTL, }, - - }, backingMemoryCache); - + + }, _backingCache); } + /// /// Return the metadata associated to this provider. /// @@ -176,7 +177,7 @@ public override async Task> ResolveStringValueAsync(st try { var key = GenerateCacheKey(flagKey, context); - return await _cache.GetOrSetAsync>(key, async (_,_) => + return await _cache.GetOrSetAsync>(key, async (_, _) => { var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String)) @@ -213,8 +214,8 @@ public override async Task> ResolveIntegerValueAsync(stri try { var key = GenerateCacheKey(flagKey, context); - return await _cache.GetOrSetAsync>(key, async (_,_) => - { + return await _cache.GetOrSetAsync>(key, async (_, _) => + { var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); return new ResolutionDetails(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None, resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); @@ -249,7 +250,7 @@ public override async Task> ResolveDoubleValueAsync(st try { var key = GenerateCacheKey(flagKey, context); - return await _cache.GetOrSetAsync>(key, async (_,_) => + return await _cache.GetOrSetAsync>(key, async (_, _) => { var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); return new ResolutionDetails(flagKey, @@ -286,7 +287,7 @@ public override async Task> ResolveStructureValueAsync( try { var key = GenerateCacheKey(flagKey, context); - return await _cache.GetOrSetAsync>(key, async (_,_) => + return await _cache.GetOrSetAsync>(key, async (_, _) => { var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false); if (resp.Value is JsonElement) @@ -358,7 +359,7 @@ private async Task CallApi(string flagKey, T defaultValue, private string GenerateCacheKey(string flagKey, EvaluationContext ctx) { - return ctx != null ? new OfrepRequest(ctx).AsJsonString() : flagKey; + return ctx != null ? flagKey + ":" + new OfrepRequest(ctx).AsJsonString() : flagKey; } /// diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs index a293e710..9057683c 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs @@ -47,12 +47,12 @@ public class GoFeatureFlagProviderOptions /// (optional) How long is an entry allowed to be cached in memory /// Default: 60 seconds /// - public TimeSpan? CacheMaxTTL { get; set; } + public TimeSpan CacheMaxTTL { get; set; } = TimeSpan.FromSeconds(60); /// /// (optional) How many entries may be cached /// Default: 10000 /// - public int? CacheMaxSize { get; set; } + public int CacheMaxSize { get; set; } = 10000; } } diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj index 8552abc2..6c3782c1 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj @@ -16,7 +16,7 @@ - + diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs index d52b90b7..411a4606 100644 --- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs +++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Threading.Tasks; using Newtonsoft.Json.Linq; +using NSubstitute; using OpenFeature.Constant; using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; using OpenFeature.Contrib.Providers.GOFeatureFlag.models; @@ -17,14 +18,16 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test; public class GoFeatureFlagProviderTest { + private static readonly string mediaType = "application/json"; private static readonly string baseUrl = "http://gofeatureflag.org"; private static readonly string prefixEval = baseUrl + "/ofrep/v1/evaluate/flags/"; private readonly EvaluationContext _defaultEvaluationCtx = InitDefaultEvaluationCtx(); - private readonly HttpMessageHandler _mockHttp = InitMock(); - - private static HttpMessageHandler InitMock() - { - const string mediaType = "application/json"; + private readonly MockHttpMessageHandler _mockHttp = InitMock(); + + + private static MockHttpMessageHandler InitMock() + { + var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{prefixEval}fail_500").Respond(HttpStatusCode.InternalServerError); mockHttp.When($"{prefixEval}api_key_missing").Respond(HttpStatusCode.BadRequest); @@ -59,8 +62,15 @@ private static HttpMessageHandler InitMock() mockHttp.When($"{prefixEval}does_not_exists").Respond(mediaType, "{ \"value\":\"\", \"key\":\"does_not_exists\", \"errorCode\":\"FLAG_NOT_FOUND\", \"variant\":\"defaultSdk\", \"cacheable\":true, \"errorDetails\":\"flag does_not_exists was not found in your configuration\"}"); mockHttp.When($"{prefixEval}integer_with_metadata").Respond(mediaType, - "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true, \"metadata\":{\"key1\": \"key1\", \"key2\": 1, \"key3\": 1.345, \"key4\": true}}"); + "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true, \"metadata\":{\"key1\": \"key1\", \"key2\": 1, \"key3\": 1.345, \"key4\": true}}"); + for (int i = 0; i < 5; i++) + { + mockHttp.When($"{prefixEval}string_key{i}").Respond(mediaType, + $"{{ \"value\":\"C{i}\", \"key\":\"string_key{i}\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}}"); + } + return mockHttp; + } private static EvaluationContext InitDefaultEvaluationCtx() @@ -300,8 +310,89 @@ public async Task should_throw_an_error_if_we_expect_a_string_and_got_another_ty Assert.Equal("default", result.Value); Assert.Equal(ErrorType.TypeMismatch, result.ErrorType); Assert.Equal(Reason.Error, result.Reason); - } + } + + [Fact] + public async Task should_cache_2nd_call() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + var req = _mockHttp.When($"{prefixEval}string_key_cache").Respond(mediaType, + "{ \"value\":\"CC00AA\", \"key\":\"string_key_cache\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"); + Assert.Equal(0, _mockHttp.GetMatchCount(req)); + + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var result = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx); + Assert.NotNull(result); + Assert.Equal("CC00AA", result.Value); + Assert.Equal(1, _mockHttp.GetMatchCount(req)); + + var result2 = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx); + Assert.NotNull(result2); + Assert.Equal("CC00AA", result.Value); + Assert.Equal(1, _mockHttp.GetMatchCount(req)); // 2nd lookup should hit cache and not activate http + } + + + + [Fact] + public async Task should_limit_cached_items() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond), + CacheMaxSize = 2 + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + + for (var i = 0; i < 5; i++) + { + var res1 = await client.GetStringDetailsAsync($"string_key{i}", "defaultValue", _defaultEvaluationCtx); + Assert.NotNull(res1); + Assert.Equal($"C{i}", res1.Value); + } + + Assert.Equal(2, g._backingCache.Count); + } + + [Fact] + public async Task should_not_cache() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond), + CacheMaxTTL = TimeSpan.FromSeconds(0) + }); + var req = _mockHttp.When($"{prefixEval}string_key_cache").Respond(mediaType, + "{ \"value\":\"CC00AA\", \"key\":\"string_key_cache\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"); + + + Assert.Equal(0, _mockHttp.GetMatchCount(req)); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var result = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx); + Assert.NotNull(result); + Assert.Equal("CC00AA", result.Value); + Assert.Equal(1, _mockHttp.GetMatchCount(req)); + + var result2 = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx); + Assert.NotNull(result2); + Assert.Equal("CC00AA", result.Value); + Assert.Equal(2, _mockHttp.GetMatchCount(req)); // 2nd lookup should not be cached, but hit http bringing total matches up + } + + [Fact] public async Task should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason() { @@ -387,6 +478,7 @@ public async Task should_use_integer_default_value_if_the_flag_is_disabled() await Api.Instance.SetProviderAsync(g); var client = Api.Instance.GetClient("test-client"); var result = await client.GetIntegerDetailsAsync("disabled_integer", 1225, _defaultEvaluationCtx); + Assert.NotNull(result); Assert.Equal(1225, result.Value); Assert.Equal(Reason.Disabled, result.Reason); From 533a4a1f1f470c157ee41855ce3621407782b10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20H=C3=B8rup?= Date: Sun, 11 May 2025 20:24:20 +0200 Subject: [PATCH 3/3] use evaluation context hashcode as part of cache key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Torben Hørup --- .../GoFeatureFlagProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs index dd151924..3d8d57cb 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs @@ -359,7 +359,7 @@ private async Task CallApi(string flagKey, T defaultValue, private string GenerateCacheKey(string flagKey, EvaluationContext ctx) { - return ctx != null ? flagKey + ":" + new OfrepRequest(ctx).AsJsonString() : flagKey; + return ctx != null ? flagKey + ":" + new OfrepRequest(ctx).AsJsonString().GetHashCode() : flagKey; } ///