From 00aa4b2b61e18cfe8fd7c61f42ac8e3e088d8859 Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 12 Aug 2026 16:01:47 +0300 Subject: [PATCH 1/7] test(ffe): enable .NET agentless configuration source --- manifests/dotnet.yml | 2 +- .../dotnet/parametric/ApmTestApi.csproj | 1 + .../dotnet/parametric/Endpoints/FfeTestApi.cs | 191 ++++++++++++++++++ .../build/docker/dotnet/parametric/Program.cs | 1 + 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 7fb1ca12d51..d38f621e2d1 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -847,7 +847,7 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Ignore: v3.10.0 tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) - tests/parametric/test_ffe/test_configuration_sources.py: missing_feature (FFL-2703 tracks .NET agentless configuration-source implementation; FFL-2731 tracks system-tests configuration-source contract) + tests/parametric/test_ffe/test_configuration_sources.py: '>=3.x.x' # FFL-2703 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: '>=3.36.0' # Modified by easy win activation script tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script tests/parametric/test_ffe/test_span_enrichment.py: missing_feature diff --git a/utils/build/docker/dotnet/parametric/ApmTestApi.csproj b/utils/build/docker/dotnet/parametric/ApmTestApi.csproj index 9b1f4807601..ff211c40fe3 100644 --- a/utils/build/docker/dotnet/parametric/ApmTestApi.csproj +++ b/utils/build/docker/dotnet/parametric/ApmTestApi.csproj @@ -13,6 +13,7 @@ + diff --git a/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs new file mode 100644 index 00000000000..cae105a336f --- /dev/null +++ b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs @@ -0,0 +1,191 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenFeature; +using OpenFeature.Constant; +using OpenFeature.Model; + +namespace ApmTestApi.Endpoints; + +public abstract class FfeTestApi +{ + private static Client? _client; + private static ILogger? _logger; + + public static void MapFfeEndpoints(WebApplication app, ILogger logger) + { + _logger = logger; + + app.MapPost("/ffe/start", StartFfe); + app.MapPost("/ffe/evaluate", EvaluateFfe); + } + + private static IResult StartFfe() + { + try + { + _logger?.LogInformation("Initializing FFE provider"); + + var provider = new DatadogProvider(); + Api.Instance.SetProvider(provider); + _client = Api.Instance.GetClient(); + + return Results.Ok(); + } + catch (Exception e) + { + _logger?.LogError(e, "Error starting FFE provider"); + return Results.Json(new { error = e.Message }, statusCode: 500); + } + } + + private static async Task EvaluateFfe(HttpRequest request) + { + if (_client is null) + { + return Results.Json(new { error = "FFE provider not initialized" }, statusCode: 500); + } + + try + { + using var jsonDoc = await JsonDocument.ParseAsync(request.Body); + var root = jsonDoc.RootElement; + + var flag = root.GetProperty("flag").GetString()!; + var variationType = root.GetProperty("variationType").GetString()!; + var targetingKey = root.GetProperty("targetingKey").GetString()!; + var attributes = new Dictionary(); + + if (root.TryGetProperty("attributes", out var attrsEl) && attrsEl.ValueKind == JsonValueKind.Object) + { + foreach (var prop in attrsEl.EnumerateObject()) + { + attributes[prop.Name] = prop.Value.ValueKind switch + { + JsonValueKind.String => prop.Value.GetString(), + JsonValueKind.Number => prop.Value.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + _ => prop.Value.GetRawText() + }; + } + } + + var context = EvaluationContext.Builder() + .SetTargetingKey(targetingKey) + .Build(); + + foreach (var (key, value) in attributes) + { + context = context.Set(key, value switch + { + string s => s, + double d => d, + bool b => b, + _ => value?.ToString() ?? string.Empty + }); + } + + object? value; + string? errorCode = null; + string reason = "DEFAULT"; + + try + { + switch (variationType) + { + case "BOOLEAN": + { + var details = await _client.ResolveBooleanValueAsync(flag, root.GetProperty("defaultValue").GetBoolean(), context); + value = details.Value; + reason = details.Reason ?? "DEFAULT"; + errorCode = ErrorTypeToString(details.ErrorCode); + } + break; + case "STRING": + { + var details = await _client.ResolveStringValueAsync(flag, root.GetProperty("defaultValue").GetString()!, context); + value = details.Value; + reason = details.Reason ?? "DEFAULT"; + errorCode = ErrorTypeToString(details.ErrorCode); + } + break; + case "INTEGER": + { + var details = await _client.ResolveIntegerValueAsync(flag, root.GetProperty("defaultValue").GetInt32(), context); + value = details.Value; + reason = details.Reason ?? "DEFAULT"; + errorCode = ErrorTypeToString(details.ErrorCode); + } + break; + case "NUMERIC": + { + var details = await _client.ResolveDoubleValueAsync(flag, root.GetProperty("defaultValue").GetDouble(), context); + value = details.Value; + reason = details.Reason ?? "DEFAULT"; + errorCode = ErrorTypeToString(details.ErrorCode); + } + break; + case "JSON": + { + var details = await _client.ResolveStructureValueAsync(flag, new Value(root.GetProperty("defaultValue").GetRawText()), context); + value = details.Value; + reason = details.Reason ?? "DEFAULT"; + errorCode = ErrorTypeToString(details.ErrorCode); + } + break; + default: + value = root.GetProperty("defaultValue").GetRawText(); + break; + } + } + catch (Exception) + { + value = GetDefaultValue(root); + reason = "ERROR"; + } + + return Results.Ok(new { value, reason, errorCode }); + } + catch (Exception e) + { + _logger?.LogError(e, "Error evaluating flag"); + return Results.Json(new { error = e.Message }, statusCode: 500); + } + } + + private static object? GetDefaultValue(JsonElement root) + { + if (!root.TryGetProperty("defaultValue", out var dv)) + return null; + + return dv.ValueKind switch + { + JsonValueKind.String => dv.GetString(), + JsonValueKind.Number => dv.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + _ => dv.GetRawText() + }; + } + + private static string? ErrorTypeToString(ErrorType errorType) + { + if (errorType == ErrorType.None) + { + return null; + } + + // Convert PascalCase enum to UPPER_SNAKE_CASE (e.g. ProviderNotReady -> PROVIDER_NOT_READY) + var name = errorType.ToString(); + var result = new System.Text.StringBuilder(); + for (var i = 0; i < name.Length; i++) + { + if (i > 0 && char.IsUpper(name[i])) + { + result.Append('_'); + } + result.Append(char.ToUpper(name[i])); + } + return result.ToString(); + } +} diff --git a/utils/build/docker/dotnet/parametric/Program.cs b/utils/build/docker/dotnet/parametric/Program.cs index ffc43036ed8..54e10f92bd4 100644 --- a/utils/build/docker/dotnet/parametric/Program.cs +++ b/utils/build/docker/dotnet/parametric/Program.cs @@ -11,6 +11,7 @@ // Map endpoints ApmTestApi.Endpoints.ApmTestApi.MapApmTraceEndpoints(app, logger); ApmTestApi.Endpoints.ApmTestApiOtel.MapApmOtelEndpoints(app, otelLogger); +ApmTestApi.Endpoints.FfeTestApi.MapFfeEndpoints(app, logger); if (!int.TryParse(Environment.GetEnvironmentVariable("APM_TEST_CLIENT_SERVER_PORT"), out var port)) { From a5f255aa8f97711a7042103a91fb0742ad8a6aeb Mon Sep 17 00:00:00 2001 From: Pavel Date: Fri, 28 Aug 2026 16:32:05 +0300 Subject: [PATCH 2/7] [FeatureFlags] Trigger system-tests base image build From 8cba8187af052c5a408d78a592383fe69e79a1ea Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 23 Sep 2026 12:07:50 +0300 Subject: [PATCH 3/7] test(ffe): await SetProviderAsync and require dotnet 3.54.0 --- manifests/dotnet.yml | 2 +- .../build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index ad539256e2d..4159be527ac 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -959,7 +959,7 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Ignore: v3.10.0 tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) - tests/parametric/test_ffe/test_configuration_sources.py: '>=3.x.x' # FFL-2703 + tests/parametric/test_ffe/test_configuration_sources.py: '>=3.54.0' # FFL-2703 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: '>=3.36.0' # Modified by easy win activation script tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script tests/parametric/test_ffe/test_span_enrichment.py: missing_feature diff --git a/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs index cae105a336f..6fbbf3bff6f 100644 --- a/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs +++ b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs @@ -19,14 +19,16 @@ public static void MapFfeEndpoints(WebApplication app, ILogger logger) app.MapPost("/ffe/evaluate", EvaluateFfe); } - private static IResult StartFfe() + private static async Task StartFfe() { try { _logger?.LogInformation("Initializing FFE provider"); var provider = new DatadogProvider(); - Api.Instance.SetProvider(provider); + + // Only the async call runs InitializeAsync, which waits for the first configuration. + await Api.Instance.SetProviderAsync(provider); _client = Api.Instance.GetClient(); return Results.Ok(); From 1f992d7a9b8e4108ac884bb687a93ef5af4ebab8 Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 23 Sep 2026 12:26:59 +0300 Subject: [PATCH 4/7] test(ffe): fix the dotnet parametric FFE endpoints against OpenFeature 2.x --- .../dotnet/parametric/Endpoints/FfeTestApi.cs | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs index 6fbbf3bff6f..920ed577141 100644 --- a/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs +++ b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs @@ -8,7 +8,7 @@ namespace ApmTestApi.Endpoints; public abstract class FfeTestApi { - private static Client? _client; + private static FeatureClient? _client; private static ILogger? _logger; public static void MapFfeEndpoints(WebApplication app, ILogger logger) @@ -25,7 +25,7 @@ private static async Task StartFfe() { _logger?.LogInformation("Initializing FFE provider"); - var provider = new DatadogProvider(); + var provider = new Datadog.FeatureFlags.OpenFeature.DatadogProvider(); // Only the async call runs InitializeAsync, which waits for the first configuration. await Api.Instance.SetProviderAsync(provider); @@ -72,21 +72,29 @@ private static async Task EvaluateFfe(HttpRequest request) } } - var context = EvaluationContext.Builder() - .SetTargetingKey(targetingKey) - .Build(); + var contextBuilder = EvaluationContext.Builder().SetTargetingKey(targetingKey); - foreach (var (key, value) in attributes) + foreach (var (key, attribute) in attributes) { - context = context.Set(key, value switch + switch (attribute) { - string s => s, - double d => d, - bool b => b, - _ => value?.ToString() ?? string.Empty - }); + case string s: + contextBuilder.Set(key, s); + break; + case double d: + contextBuilder.Set(key, d); + break; + case bool b: + contextBuilder.Set(key, b); + break; + default: + contextBuilder.Set(key, attribute?.ToString() ?? string.Empty); + break; + } } + var context = contextBuilder.Build(); + object? value; string? errorCode = null; string reason = "DEFAULT"; @@ -97,42 +105,42 @@ private static async Task EvaluateFfe(HttpRequest request) { case "BOOLEAN": { - var details = await _client.ResolveBooleanValueAsync(flag, root.GetProperty("defaultValue").GetBoolean(), context); + var details = await _client.GetBooleanDetailsAsync(flag, root.GetProperty("defaultValue").GetBoolean(), context); value = details.Value; reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorCode); + errorCode = ErrorTypeToString(details.ErrorType); } break; case "STRING": { - var details = await _client.ResolveStringValueAsync(flag, root.GetProperty("defaultValue").GetString()!, context); + var details = await _client.GetStringDetailsAsync(flag, root.GetProperty("defaultValue").GetString()!, context); value = details.Value; reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorCode); + errorCode = ErrorTypeToString(details.ErrorType); } break; case "INTEGER": { - var details = await _client.ResolveIntegerValueAsync(flag, root.GetProperty("defaultValue").GetInt32(), context); + var details = await _client.GetIntegerDetailsAsync(flag, root.GetProperty("defaultValue").GetInt32(), context); value = details.Value; reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorCode); + errorCode = ErrorTypeToString(details.ErrorType); } break; case "NUMERIC": { - var details = await _client.ResolveDoubleValueAsync(flag, root.GetProperty("defaultValue").GetDouble(), context); + var details = await _client.GetDoubleDetailsAsync(flag, root.GetProperty("defaultValue").GetDouble(), context); value = details.Value; reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorCode); + errorCode = ErrorTypeToString(details.ErrorType); } break; case "JSON": { - var details = await _client.ResolveStructureValueAsync(flag, new Value(root.GetProperty("defaultValue").GetRawText()), context); + var details = await _client.GetObjectDetailsAsync(flag, new Value(root.GetProperty("defaultValue").GetRawText()), context); value = details.Value; reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorCode); + errorCode = ErrorTypeToString(details.ErrorType); } break; default: From 5fe83f9fd4c4aa03dce0af83630507b03dd8a4a2 Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 23 Sep 2026 16:27:54 +0300 Subject: [PATCH 5/7] test(ffe): mark dotnet cold-start cases as bug until FFL-3325 ships --- manifests/dotnet.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 4159be527ac..fb9bb80d278 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -960,6 +960,11 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) tests/parametric/test_ffe/test_configuration_sources.py: '>=3.54.0' # FFL-2703 + tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_bad_to_unchanged_cold_preserves_not_ready: bug (FFL-3325) + tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_malformed_cold: bug (FFL-3325) + tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_request_timeout_cold: bug (FFL-3325) + tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_unauthorized_cold: bug (FFL-3325) + tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Selection::test_remote_config_without_rc_does_not_fallback_to_agentless: bug (FFL-3325) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: '>=3.36.0' # Modified by easy win activation script tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script tests/parametric/test_ffe/test_span_enrichment.py: missing_feature From 29010b96903eaa3c46fe3682af4a5cedbbff1050 Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 23 Sep 2026 17:26:11 +0300 Subject: [PATCH 6/7] test(ffe): use long-key form for the dotnet cold-start manifest entries --- manifests/dotnet.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index fb9bb80d278..4e411357957 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -960,11 +960,16 @@ manifest: tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage) tests/parametric/test_ffe/test_configuration_sources.py: '>=3.54.0' # FFL-2703 - tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_bad_to_unchanged_cold_preserves_not_ready: bug (FFL-3325) - tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_malformed_cold: bug (FFL-3325) - tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_request_timeout_cold: bug (FFL-3325) - tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_unauthorized_cold: bug (FFL-3325) - tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Selection::test_remote_config_without_rc_does_not_fallback_to_agentless: bug (FFL-3325) + ? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_bad_to_unchanged_cold_preserves_not_ready + : bug (FFL-3325) + ? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_malformed_cold + : bug (FFL-3325) + ? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_request_timeout_cold + : bug (FFL-3325) + ? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_unauthorized_cold + : bug (FFL-3325) + ? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Selection::test_remote_config_without_rc_does_not_fallback_to_agentless + : bug (FFL-3325) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: '>=3.36.0' # Modified by easy win activation script tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script tests/parametric/test_ffe/test_span_enrichment.py: missing_feature From 03c4320f0ff8de79d02da0a6cde64fdefa46d364 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 24 Sep 2026 17:24:03 +0300 Subject: [PATCH 7/7] test(ffe): address review on the dotnet parametric FFE endpoints --- .github/CODEOWNERS | 2 + .../dotnet/parametric/ApmTestApi.csproj | 2 +- .../dotnet/parametric/Endpoints/FfeTestApi.cs | 189 +++++------------- 3 files changed, 52 insertions(+), 141 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 21a14394c54..3cada739baf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,8 @@ # FFE /utils/docker_fixtures/_mock_ffe_agentless_backend.py @DataDog/feature-flagging-and-experimentation-sdk @DataDog/system-tests-core +/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs @DataDog/feature-flagging-and-experimentation-sdk @DataDog/system-tests-core +/utils/build/docker/dotnet/weblog/Controllers/FeatureFlagEvaluatorController.cs @DataDog/feature-flagging-and-experimentation-sdk @DataDog/system-tests-core /tests/ffe/ @DataDog/feature-flagging-and-experimentation-sdk /tests/test_the_test/test_mock_ffe_agentless_backend.py @DataDog/feature-flagging-and-experimentation-sdk /tests/parametric/test_ffe/ @DataDog/feature-flagging-and-experimentation-sdk diff --git a/utils/build/docker/dotnet/parametric/ApmTestApi.csproj b/utils/build/docker/dotnet/parametric/ApmTestApi.csproj index ff211c40fe3..b2fcbc99bf0 100644 --- a/utils/build/docker/dotnet/parametric/ApmTestApi.csproj +++ b/utils/build/docker/dotnet/parametric/ApmTestApi.csproj @@ -13,7 +13,7 @@ - + diff --git a/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs index 920ed577141..5c6a642da30 100644 --- a/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs +++ b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs @@ -1,5 +1,6 @@ +using System.ComponentModel; +using System.Reflection; using System.Text.Json; -using System.Text.Json.Serialization; using OpenFeature; using OpenFeature.Constant; using OpenFeature.Model; @@ -40,162 +41,70 @@ private static async Task StartFfe() } } - private static async Task EvaluateFfe(HttpRequest request) + private static async Task EvaluateFfe(EvaluateRequest request) { if (_client is null) { return Results.Json(new { error = "FFE provider not initialized" }, statusCode: 500); } - try - { - using var jsonDoc = await JsonDocument.ParseAsync(request.Body); - var root = jsonDoc.RootElement; - - var flag = root.GetProperty("flag").GetString()!; - var variationType = root.GetProperty("variationType").GetString()!; - var targetingKey = root.GetProperty("targetingKey").GetString()!; - var attributes = new Dictionary(); - - if (root.TryGetProperty("attributes", out var attrsEl) && attrsEl.ValueKind == JsonValueKind.Object) - { - foreach (var prop in attrsEl.EnumerateObject()) - { - attributes[prop.Name] = prop.Value.ValueKind switch - { - JsonValueKind.String => prop.Value.GetString(), - JsonValueKind.Number => prop.Value.GetDouble(), - JsonValueKind.True => true, - JsonValueKind.False => false, - _ => prop.Value.GetRawText() - }; - } - } + var contextBuilder = EvaluationContext.Builder().SetTargetingKey(request.TargetingKey); - var contextBuilder = EvaluationContext.Builder().SetTargetingKey(targetingKey); - - foreach (var (key, attribute) in attributes) + foreach (var (key, attribute) in request.Attributes ?? new()) + { + switch (attribute.ValueKind) { - switch (attribute) - { - case string s: - contextBuilder.Set(key, s); - break; - case double d: - contextBuilder.Set(key, d); - break; - case bool b: - contextBuilder.Set(key, b); - break; - default: - contextBuilder.Set(key, attribute?.ToString() ?? string.Empty); - break; - } + case JsonValueKind.String: + contextBuilder.Set(key, attribute.GetString()!); + break; + case JsonValueKind.Number: + contextBuilder.Set(key, attribute.GetDouble()); + break; + case JsonValueKind.True: + case JsonValueKind.False: + contextBuilder.Set(key, attribute.GetBoolean()); + break; + default: + contextBuilder.Set(key, attribute.GetRawText()); + break; } + } - var context = contextBuilder.Build(); - - object? value; - string? errorCode = null; - string reason = "DEFAULT"; + var context = contextBuilder.Build(); + var defaultValue = request.DefaultValue; - try - { - switch (variationType) - { - case "BOOLEAN": - { - var details = await _client.GetBooleanDetailsAsync(flag, root.GetProperty("defaultValue").GetBoolean(), context); - value = details.Value; - reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorType); - } - break; - case "STRING": - { - var details = await _client.GetStringDetailsAsync(flag, root.GetProperty("defaultValue").GetString()!, context); - value = details.Value; - reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorType); - } - break; - case "INTEGER": - { - var details = await _client.GetIntegerDetailsAsync(flag, root.GetProperty("defaultValue").GetInt32(), context); - value = details.Value; - reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorType); - } - break; - case "NUMERIC": - { - var details = await _client.GetDoubleDetailsAsync(flag, root.GetProperty("defaultValue").GetDouble(), context); - value = details.Value; - reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorType); - } - break; - case "JSON": - { - var details = await _client.GetObjectDetailsAsync(flag, new Value(root.GetProperty("defaultValue").GetRawText()), context); - value = details.Value; - reason = details.Reason ?? "DEFAULT"; - errorCode = ErrorTypeToString(details.ErrorType); - } - break; - default: - value = root.GetProperty("defaultValue").GetRawText(); - break; - } - } - catch (Exception) + try + { + return request.VariationType switch { - value = GetDefaultValue(root); - reason = "ERROR"; - } - - return Results.Ok(new { value, reason, errorCode }); + "BOOLEAN" => ToResult(await _client.GetBooleanDetailsAsync(request.Flag, defaultValue.GetBoolean(), context)), + "STRING" => ToResult(await _client.GetStringDetailsAsync(request.Flag, defaultValue.GetString()!, context)), + "INTEGER" => ToResult(await _client.GetIntegerDetailsAsync(request.Flag, defaultValue.GetInt32(), context)), + "NUMERIC" => ToResult(await _client.GetDoubleDetailsAsync(request.Flag, defaultValue.GetDouble(), context)), + "JSON" => ToResult(await _client.GetObjectDetailsAsync(request.Flag, new Value(defaultValue.GetRawText()), context)), + _ => Results.Ok(new { value = defaultValue, reason = "DEFAULT", errorCode = (string?)null }), + }; } catch (Exception e) { _logger?.LogError(e, "Error evaluating flag"); - return Results.Json(new { error = e.Message }, statusCode: 500); + return Results.Ok(new { value = defaultValue, reason = "ERROR", errorCode = (string?)null }); } } - private static object? GetDefaultValue(JsonElement root) - { - if (!root.TryGetProperty("defaultValue", out var dv)) - return null; - - return dv.ValueKind switch - { - JsonValueKind.String => dv.GetString(), - JsonValueKind.Number => dv.GetDouble(), - JsonValueKind.True => true, - JsonValueKind.False => false, - _ => dv.GetRawText() - }; - } - - private static string? ErrorTypeToString(ErrorType errorType) - { - if (errorType == ErrorType.None) - { - return null; - } - - // Convert PascalCase enum to UPPER_SNAKE_CASE (e.g. ProviderNotReady -> PROVIDER_NOT_READY) - var name = errorType.ToString(); - var result = new System.Text.StringBuilder(); - for (var i = 0; i < name.Length; i++) - { - if (i > 0 && char.IsUpper(name[i])) - { - result.Append('_'); - } - result.Append(char.ToUpper(name[i])); - } - return result.ToString(); - } + private static IResult ToResult(FlagEvaluationDetails details) + => Results.Ok(new { value = details.Value, reason = details.Reason ?? "DEFAULT", errorCode = ToErrorCode(details.ErrorType) }); + + // OpenFeature carries the spec error code on each ErrorType member as its Description. + private static string? ToErrorCode(ErrorType errorType) + => errorType == ErrorType.None + ? null + : typeof(ErrorType).GetField(errorType.ToString())?.GetCustomAttribute()?.Description; + + public sealed record EvaluateRequest( + string Flag, + string VariationType, + JsonElement DefaultValue, + string TargetingKey, + Dictionary? Attributes); }