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/manifests/dotnet.yml b/manifests/dotnet.yml
index 8e0043a7794..102373a27a2 100644
--- a/manifests/dotnet.yml
+++ b/manifests/dotnet.yml
@@ -960,7 +960,17 @@ 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.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
diff --git a/utils/build/docker/dotnet/parametric/ApmTestApi.csproj b/utils/build/docker/dotnet/parametric/ApmTestApi.csproj
index 9b1f4807601..b2fcbc99bf0 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..5c6a642da30
--- /dev/null
+++ b/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs
@@ -0,0 +1,110 @@
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json;
+using OpenFeature;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+namespace ApmTestApi.Endpoints;
+
+public abstract class FfeTestApi
+{
+ private static FeatureClient? _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 async Task StartFfe()
+ {
+ try
+ {
+ _logger?.LogInformation("Initializing FFE provider");
+
+ var provider = new Datadog.FeatureFlags.OpenFeature.DatadogProvider();
+
+ // Only the async call runs InitializeAsync, which waits for the first configuration.
+ await Api.Instance.SetProviderAsync(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(EvaluateRequest request)
+ {
+ if (_client is null)
+ {
+ return Results.Json(new { error = "FFE provider not initialized" }, statusCode: 500);
+ }
+
+ var contextBuilder = EvaluationContext.Builder().SetTargetingKey(request.TargetingKey);
+
+ foreach (var (key, attribute) in request.Attributes ?? new())
+ {
+ switch (attribute.ValueKind)
+ {
+ 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();
+ var defaultValue = request.DefaultValue;
+
+ try
+ {
+ return request.VariationType switch
+ {
+ "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.Ok(new { value = defaultValue, reason = "ERROR", errorCode = (string?)null });
+ }
+ }
+
+ 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);
+}
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))
{