diff --git a/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs b/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs
index 79c9667eb38..7f6a3ee66a9 100644
--- a/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs
+++ b/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs
@@ -5,6 +5,7 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
namespace Aspire.Hosting.RemoteHost.Ats;
@@ -49,7 +50,8 @@ internal sealed class UnmarshalContext
private static readonly JsonSerializerOptions s_jsonOptions = new()
{
PropertyNameCaseInsensitive = true,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() }
};
///
diff --git a/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs b/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs
index 72e29f9d62f..2af40e8523d 100644
--- a/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs
+++ b/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs
@@ -533,11 +533,6 @@ private static bool IsTypeMismatchException(ArgumentException ex)
///
internal static class CapabilityJsonExtensions
{
- private static readonly JsonSerializerOptions s_jsonOptions = new()
- {
- PropertyNameCaseInsensitive = true,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
///
/// Gets a required string argument.
@@ -615,7 +610,7 @@ public static T GetRequiredHandle(
{
if (args.TryGetPropertyValue(name, out var node) && node is JsonObject obj)
{
- return JsonSerializer.Deserialize(obj.ToJsonString(), s_jsonOptions);
+ return JsonSerializer.Deserialize(obj.ToJsonString(), AtsMarshaller.JsonOptions);
}
return null;
}
diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/AtsMarshallerTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/AtsMarshallerTests.cs
index 77d8793a87e..ff8756b73f8 100644
--- a/tests/Aspire.Hosting.RemoteHost.Tests/AtsMarshallerTests.cs
+++ b/tests/Aspire.Hosting.RemoteHost.Tests/AtsMarshallerTests.cs
@@ -16,7 +16,10 @@ private static AtsContext CreateTestContext()
{
Capabilities = [],
HandleTypes = [],
- DtoTypes = [new AtsDtoTypeInfo { TypeId = "test/TestDto", Name = "TestDto", ClrType = typeof(TestDto), Properties = [] }],
+ DtoTypes = [
+ new AtsDtoTypeInfo { TypeId = "test/TestDto", Name = "TestDto", ClrType = typeof(TestDto), Properties = [] },
+ new AtsDtoTypeInfo { TypeId = "test/TestDtoWithEnum", Name = "TestDtoWithEnum", ClrType = typeof(TestDtoWithEnum), Properties = [] }
+ ],
EnumTypes = []
};
}
@@ -560,6 +563,56 @@ public void MarshalToJson_MarshalsDto()
Assert.Equal(10, jsonObj["count"]?.GetValue());
}
+ [Fact]
+ public void JsonOptions_ContainsJsonStringEnumConverter()
+ {
+ var options = AtsMarshaller.JsonOptions;
+
+ Assert.Contains(options.Converters, c => c is System.Text.Json.Serialization.JsonStringEnumConverter);
+ }
+
+ [Fact]
+ public void MarshalToJson_MarshalsDtoWithEnumPropertyAsString()
+ {
+ var marshaller = CreateMarshaller();
+ var dto = new TestDtoWithEnum { Label = "item", Status = TestEnum.ValueB };
+
+ var result = marshaller.MarshalToJson(dto);
+
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ var jsonObj = (JsonObject)result;
+ Assert.Equal("item", jsonObj["label"]?.GetValue());
+ Assert.Equal("ValueB", jsonObj["status"]?.GetValue());
+ }
+
+ [Fact]
+ public void UnmarshalFromJson_UnmarshalsDtoWithEnumPropertyFromString()
+ {
+ var (marshaller, context) = CreateMarshallerWithContext();
+ var json = new JsonObject { ["label"] = "item", ["status"] = "ValueA" };
+
+ var result = marshaller.UnmarshalFromJson(json, typeof(TestDtoWithEnum), context);
+
+ Assert.NotNull(result);
+ var dto = Assert.IsType(result);
+ Assert.Equal("item", dto.Label);
+ Assert.Equal(TestEnum.ValueA, dto.Status);
+ }
+
+ [Fact]
+ public void UnmarshalFromJson_UnmarshalsDtoWithEnumPropertyCaseInsensitive()
+ {
+ var (marshaller, context) = CreateMarshallerWithContext();
+ var json = new JsonObject { ["label"] = "test", ["status"] = "valueb" };
+
+ var result = marshaller.UnmarshalFromJson(json, typeof(TestDtoWithEnum), context);
+
+ Assert.NotNull(result);
+ var dto = Assert.IsType(result);
+ Assert.Equal(TestEnum.ValueB, dto.Status);
+ }
+
[Fact]
public void UnmarshalFromJson_UnmarshalsEnumFromString()
{
@@ -643,4 +696,11 @@ private sealed class TestDto
public string? Name { get; set; }
public int Count { get; set; }
}
+
+ [AspireDto]
+ private sealed class TestDtoWithEnum
+ {
+ public string? Label { get; set; }
+ public TestEnum Status { get; set; }
+ }
}
diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs
index 6f20ddb2cd6..40becadefb4 100644
--- a/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs
+++ b/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs
@@ -1153,6 +1153,53 @@ public void Invoke_AcceptOptionalEnum_WithoutValue()
Assert.Equal("No value", result.GetValue());
}
+ [Fact]
+ public void GetDto_DeserializesEnumPropertyFromString()
+ {
+ var args = new JsonObject
+ {
+ ["dto"] = new JsonObject
+ {
+ ["label"] = "test-item",
+ ["status"] = "ValueB"
+ }
+ };
+
+ var result = args.GetDto("dto");
+
+ Assert.NotNull(result);
+ Assert.Equal("test-item", result.Label);
+ Assert.Equal(TestDispatchEnum.ValueB, result.Status);
+ }
+
+ [Fact]
+ public void GetDto_DeserializesEnumPropertyFromStringCaseInsensitive()
+ {
+ var args = new JsonObject
+ {
+ ["dto"] = new JsonObject
+ {
+ ["label"] = "item",
+ ["status"] = "valuec"
+ }
+ };
+
+ var result = args.GetDto("dto");
+
+ Assert.NotNull(result);
+ Assert.Equal(TestDispatchEnum.ValueC, result.Status);
+ }
+
+ [Fact]
+ public void GetDto_ReturnsNullWhenPropertyMissing()
+ {
+ var args = new JsonObject();
+
+ var result = args.GetDto("dto");
+
+ Assert.Null(result);
+ }
+
private static CapabilityDispatcher CreateDispatcher(params System.Reflection.Assembly[] assemblies)
{
var handles = new HandleRegistry();
@@ -1431,3 +1478,12 @@ public static string AcceptOptionalEnum(TestDispatchEnum? value = null)
return value.HasValue ? $"Received: {value.Value}" : "No value";
}
}
+
+///
+/// Test DTO with an enum property for GetDto deserialization tests.
+///
+internal sealed class TestDtoWithEnum
+{
+ public string? Label { get; set; }
+ public TestDispatchEnum Status { get; set; }
+}