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; } +}