From d2546d8ba280eea809c43e99c6d567214727100b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:52:15 -0800 Subject: [PATCH 1/6] debug --- .../WebJobs.Extensions.DurableTask.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj index ce21c62d6..9280ece71 100644 --- a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj +++ b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj @@ -19,6 +19,9 @@ embedded NU5125;SA0001 + portable + true + false From f0916274e60e09b8fd053cd4337483a17a4536c4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:02:04 -0800 Subject: [PATCH 2/6] Support Polymorphic input payload deserialization --- .../ObjectConverterShim.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Worker.Extensions.DurableTask/ObjectConverterShim.cs b/src/Worker.Extensions.DurableTask/ObjectConverterShim.cs index 9f8abd884..f9b070b9a 100644 --- a/src/Worker.Extensions.DurableTask/ObjectConverterShim.cs +++ b/src/Worker.Extensions.DurableTask/ObjectConverterShim.cs @@ -3,7 +3,9 @@ using System; using System.IO; +using System.Reflection; using System.Text; +using System.Text.Json.Serialization; using Azure.Core.Serialization; using Microsoft.DurableTask; @@ -39,7 +41,69 @@ public ObjectConverterShim(ObjectSerializer serializer) return null; } - BinaryData data = this.serializer.Serialize(value, value.GetType(), default); + Type valueType = value.GetType(); + + // Special handling for object[] arrays - DTFx wraps activity inputs in object[] + // When System.Text.Json serializes object[], it treats elements as type 'object' (the static array element type), + // not as their runtime concrete type, which prevents polymorphic type discriminators from being added. + // We must serialize each element individually with its polymorphic base type and build the JSON array manually. + if (valueType == typeof(object[])) + { + object[] array = (object[])value; + var jsonBuilder = new StringBuilder(); + jsonBuilder.Append('['); + + for (int i = 0; i < array.Length; i++) + { + if (i > 0) + { + jsonBuilder.Append(','); + } + + object? element = array[i]; + if (element != null) + { + // Serialize each element with its polymorphic base type to include type discriminators + Type elementType = element.GetType(); + Type serializeAs = GetPolymorphicBaseType(elementType) ?? elementType; + + BinaryData elementData = this.serializer.Serialize(element, serializeAs, default); + jsonBuilder.Append(elementData.ToString()); + } + else + { + jsonBuilder.Append("null"); + } + } + + jsonBuilder.Append(']'); + return jsonBuilder.ToString(); + } + + // For non-array values (or non-object[] arrays), serialize as polymorphic base type + Type typeToSerialize = GetPolymorphicBaseType(valueType) ?? valueType; + + BinaryData data = this.serializer.Serialize(value, typeToSerialize, default); return data.ToString(); } + + /// + /// Finds the polymorphic base type for a given type by walking up the inheritance chain. + /// Returns the first base type decorated with . + /// + /// The concrete type to check. + /// The polymorphic base type if found, otherwise null. + private static Type? GetPolymorphicBaseType(Type concreteType) + { + Type? current = concreteType.BaseType; + while (current != null && current != typeof(object)) + { + if (current.GetCustomAttribute() != null) + { + return current; + } + current = current.BaseType; + } + return null; + } } From 77d66ec6ec14fa87929f6d975f40063f160fe39a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:40:25 -0800 Subject: [PATCH 3/6] unit tests --- .../ObjectConverterShimTests.cs | 527 ++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 test/Worker.Extensions.DurableTask.Tests/ObjectConverterShimTests.cs diff --git a/test/Worker.Extensions.DurableTask.Tests/ObjectConverterShimTests.cs b/test/Worker.Extensions.DurableTask.Tests/ObjectConverterShimTests.cs new file mode 100644 index 000000000..925df65ae --- /dev/null +++ b/test/Worker.Extensions.DurableTask.Tests/ObjectConverterShimTests.cs @@ -0,0 +1,527 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Core.Serialization; +using Microsoft.Azure.Functions.Worker.Extensions.DurableTask; +using Xunit; + +namespace Microsoft.Azure.Functions.Worker.Tests; + +/// +/// Unit tests for . +/// Tests polymorphic serialization/deserialization capabilities. +/// +public class ObjectConverterShimTests +{ + #region Test Model Classes + + [JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")] + [JsonDerivedType(typeof(Dog), "Dog")] + [JsonDerivedType(typeof(Cat), "Cat")] + [JsonDerivedType(typeof(Bird), "Bird")] + public class Animal + { + public string? Name { get; set; } + } + + public class Dog : Animal + { + public string? Breed { get; set; } + } + + public class Cat : Animal + { + public int Lives { get; set; } + } + + public class Bird : Animal + { + public double WingSpan { get; set; } + } + + public class SimpleClass + { + public string? Value { get; set; } + } + + #endregion + + private ObjectConverterShim CreateShim() + { + var jsonOptions = new JsonSerializerOptions + { + WriteIndented = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + TypeInfoResolver = new System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver() + }; + var serializer = new JsonObjectSerializer(jsonOptions); + return new ObjectConverterShim(serializer); + } + + #region Serialization Tests + + [Fact] + public void Serialize_SimpleString_ReturnsJsonString() + { + // Arrange + var shim = CreateShim(); + var value = "Hello World"; + + // Act + string? result = shim.Serialize(value); + + // Assert + Assert.NotNull(result); + Assert.Equal("\"Hello World\"", result); + } + + [Fact] + public void Serialize_Null_ReturnsNull() + { + // Arrange + var shim = CreateShim(); + + // Act + string? result = shim.Serialize(null); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Serialize_SimpleObject_ReturnsJson() + { + // Arrange + var shim = CreateShim(); + var obj = new SimpleClass { Value = "test" }; + + // Act + string? result = shim.Serialize(obj); + + // Assert + Assert.NotNull(result); + Assert.Contains("\"value\":\"test\"", result); + } + + [Fact] + public void Serialize_PolymorphicType_IncludesTypeDiscriminator() + { + // Arrange + var shim = CreateShim(); + var dog = new Dog { Name = "Buddy", Breed = "Golden Retriever" }; + + // Act + string? result = shim.Serialize(dog); + + // Assert + Assert.NotNull(result); + Assert.Contains("\"__type\":\"Dog\"", result); + Assert.Contains("\"name\":\"Buddy\"", result); + Assert.Contains("\"breed\":\"Golden Retriever\"", result); + } + + [Fact] + public void Serialize_PolymorphicCat_IncludesTypeDiscriminator() + { + // Arrange + var shim = CreateShim(); + var cat = new Cat { Name = "Whiskers", Lives = 9 }; + + // Act + string? result = shim.Serialize(cat); + + // Assert + Assert.NotNull(result); + Assert.Contains("\"__type\":\"Cat\"", result); + Assert.Contains("\"name\":\"Whiskers\"", result); + Assert.Contains("\"lives\":9", result); + } + + [Fact] + public void Serialize_ObjectArray_WithPolymorphicElements_IncludesTypeDiscriminators() + { + // Arrange + var shim = CreateShim(); + object[] animals = new object[] + { + new Dog { Name = "Rex", Breed = "Beagle" }, + new Cat { Name = "Mittens", Lives = 7 }, + new Bird { Name = "Tweety", WingSpan = 0.3 } + }; + + // Act + string? result = shim.Serialize(animals); + + // Assert + Assert.NotNull(result); + + // Should be a JSON array + Assert.StartsWith("[", result); + Assert.EndsWith("]", result); + + // Should contain type discriminators for all elements + Assert.Contains("\"__type\":\"Dog\"", result); + Assert.Contains("\"__type\":\"Cat\"", result); + Assert.Contains("\"__type\":\"Bird\"", result); + + // Should contain all properties + Assert.Contains("\"name\":\"Rex\"", result); + Assert.Contains("\"breed\":\"Beagle\"", result); + Assert.Contains("\"name\":\"Mittens\"", result); + Assert.Contains("\"lives\":7", result); + Assert.Contains("\"name\":\"Tweety\"", result); + Assert.Contains("\"wingSpan\":0.3", result); + } + + [Fact] + public void Serialize_ObjectArray_WithNull_HandlesNullElements() + { + // Arrange + var shim = CreateShim(); + object?[] array = new object?[] + { + new Dog { Name = "Max", Breed = "Poodle" }, + null, + new Cat { Name = "Shadow", Lives = 8 } + }; + + // Act + string? result = shim.Serialize(array); + + // Assert + Assert.NotNull(result); + Assert.Contains("null", result); + Assert.Contains("\"__type\":\"Dog\"", result); + Assert.Contains("\"__type\":\"Cat\"", result); + } + + [Fact] + public void Serialize_ObjectArray_WithMixedTypes_SerializesAll() + { + // Arrange + var shim = CreateShim(); + object[] mixed = new object[] + { + new Dog { Name = "Fido", Breed = "Labrador" }, + "plain string", + 42, + new SimpleClass { Value = "simple" } + }; + + // Act + string? result = shim.Serialize(mixed); + + // Assert + Assert.NotNull(result); + Assert.Contains("\"__type\":\"Dog\"", result); + Assert.Contains("\"plain string\"", result); + Assert.Contains("42", result); + Assert.Contains("\"value\":\"simple\"", result); + } + + [Fact] + public void Serialize_EmptyObjectArray_ReturnsEmptyJsonArray() + { + // Arrange + var shim = CreateShim(); + object[] empty = Array.Empty(); + + // Act + string? result = shim.Serialize(empty); + + // Assert + Assert.NotNull(result); + Assert.Equal("[]", result); + } + + #endregion + + #region Deserialization Tests + + [Fact] + public void Deserialize_JsonString_ReturnsString() + { + // Arrange + var shim = CreateShim(); + var json = "\"Hello World\""; + + // Act + object? result = shim.Deserialize(json, typeof(string)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal("Hello World", result); + } + + [Fact] + public void Deserialize_Null_ReturnsNull() + { + // Arrange + var shim = CreateShim(); + + // Act + object? result = shim.Deserialize(null, typeof(string)); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Deserialize_SimpleObject_ReturnsObject() + { + // Arrange + var shim = CreateShim(); + var json = "{\"value\":\"test\"}"; + + // Act + object? result = shim.Deserialize(json, typeof(SimpleClass)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal("test", ((SimpleClass)result).Value); + } + + [Fact] + public void Deserialize_PolymorphicDog_WithTypeDiscriminator_ReturnsDog() + { + // Arrange + var shim = CreateShim(); + var json = "{\"__type\":\"Dog\",\"name\":\"Buddy\",\"breed\":\"Golden Retriever\"}"; + + // Act + object? result = shim.Deserialize(json, typeof(Animal)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + var dog = (Dog)result; + Assert.Equal("Buddy", dog.Name); + Assert.Equal("Golden Retriever", dog.Breed); + } + + [Fact] + public void Deserialize_PolymorphicCat_WithTypeDiscriminator_ReturnsCat() + { + // Arrange + var shim = CreateShim(); + var json = "{\"__type\":\"Cat\",\"name\":\"Whiskers\",\"lives\":9}"; + + // Act + object? result = shim.Deserialize(json, typeof(Animal)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + var cat = (Cat)result; + Assert.Equal("Whiskers", cat.Name); + Assert.Equal(9, cat.Lives); + } + + [Fact] + public void Deserialize_PolymorphicBird_WithTypeDiscriminator_ReturnsBird() + { + // Arrange + var shim = CreateShim(); + var json = "{\"__type\":\"Bird\",\"name\":\"Tweety\",\"wingSpan\":0.3}"; + + // Act + object? result = shim.Deserialize(json, typeof(Animal)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + var bird = (Bird)result; + Assert.Equal("Tweety", bird.Name); + Assert.Equal(0.3, bird.WingSpan); + } + + [Fact] + public void Deserialize_PolymorphicType_WithoutTypeDiscriminator_ReturnsBaseType() + { + // Arrange + var shim = CreateShim(); + var json = "{\"name\":\"Unknown\"}"; + + // Act + object? result = shim.Deserialize(json, typeof(Animal)); + + // Assert + Assert.NotNull(result); + // Without type discriminator, should deserialize as base type + Assert.IsType(result); + var animal = (Animal)result; + Assert.Equal("Unknown", animal.Name); + } + + #endregion + + #region Round-Trip Tests + + [Fact] + public void RoundTrip_PolymorphicDog_PreservesTypeAndData() + { + // Arrange + var shim = CreateShim(); + var original = new Dog { Name = "Rex", Breed = "Beagle" }; + + // Act + string? json = shim.Serialize(original); + object? deserialized = shim.Deserialize(json, typeof(Animal)); + + // Assert + Assert.NotNull(deserialized); + Assert.IsType(deserialized); + var dog = (Dog)deserialized; + Assert.Equal(original.Name, dog.Name); + Assert.Equal(original.Breed, dog.Breed); + } + + [Fact] + public void RoundTrip_PolymorphicCat_PreservesTypeAndData() + { + // Arrange + var shim = CreateShim(); + var original = new Cat { Name = "Mittens", Lives = 7 }; + + // Act + string? json = shim.Serialize(original); + object? deserialized = shim.Deserialize(json, typeof(Animal)); + + // Assert + Assert.NotNull(deserialized); + Assert.IsType(deserialized); + var cat = (Cat)deserialized; + Assert.Equal(original.Name, cat.Name); + Assert.Equal(original.Lives, cat.Lives); + } + + [Fact] + public void RoundTrip_ObjectArrayWithPolymorphicElements_PreservesAllTypes() + { + // Arrange + var shim = CreateShim(); + object[] original = new object[] + { + new Dog { Name = "Max", Breed = "Poodle" }, + new Cat { Name = "Shadow", Lives = 8 }, + new Bird { Name = "Rio", WingSpan = 0.5 } + }; + + // Act - Serialize as object[] + string? json = shim.Serialize(original); + + // Deserialize back - need to deserialize as JsonElement first, then each element + var jsonDoc = JsonDocument.Parse(json!); + var elements = jsonDoc.RootElement.EnumerateArray().ToArray(); + + // Assert + Assert.Equal(3, elements.Length); + + // Deserialize each element individually + var dog = shim.Deserialize(elements[0].GetRawText(), typeof(Animal)); + Assert.IsType(dog); + Assert.Equal("Max", ((Dog)dog!).Name); + Assert.Equal("Poodle", ((Dog)dog).Breed); + + var cat = shim.Deserialize(elements[1].GetRawText(), typeof(Animal)); + Assert.IsType(cat); + Assert.Equal("Shadow", ((Cat)cat!).Name); + Assert.Equal(8, ((Cat)cat).Lives); + + var bird = shim.Deserialize(elements[2].GetRawText(), typeof(Animal)); + Assert.IsType(bird); + Assert.Equal("Rio", ((Bird)bird!).Name); + Assert.Equal(0.5, ((Bird)bird).WingSpan); + } + + [Fact] + public void RoundTrip_SimpleClass_PreservesData() + { + // Arrange + var shim = CreateShim(); + var original = new SimpleClass { Value = "test value" }; + + // Act + string? json = shim.Serialize(original); + object? deserialized = shim.Deserialize(json, typeof(SimpleClass)); + + // Assert + Assert.NotNull(deserialized); + Assert.IsType(deserialized); + Assert.Equal(original.Value, ((SimpleClass)deserialized).Value); + } + + #endregion + + #region Edge Cases + + [Fact] + public void Serialize_Int32_ReturnsJsonNumber() + { + // Arrange + var shim = CreateShim(); + int value = 42; + + // Act + string? result = shim.Serialize(value); + + // Assert + Assert.NotNull(result); + Assert.Equal("42", result); + } + + [Fact] + public void Serialize_Boolean_ReturnsJsonBoolean() + { + // Arrange + var shim = CreateShim(); + bool value = true; + + // Act + string? result = shim.Serialize(value); + + // Assert + Assert.NotNull(result); + Assert.Equal("true", result); + } + + [Fact] + public void Deserialize_Int32_ReturnsInt() + { + // Arrange + var shim = CreateShim(); + string json = "42"; + + // Act + object? result = shim.Deserialize(json, typeof(int)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(42, result); + } + + [Fact] + public void Deserialize_Boolean_ReturnsBool() + { + // Arrange + var shim = CreateShim(); + string json = "true"; + + // Act + object? result = shim.Deserialize(json, typeof(bool)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.True((bool)result); + } + + #endregion +} + From ae2f6323eeea40416946f065d0593064364b8cbc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:44:44 -0800 Subject: [PATCH 4/6] revert --- .../WebJobs.Extensions.DurableTask.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj index 9280ece71..ce21c62d6 100644 --- a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj +++ b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj @@ -19,9 +19,6 @@ embedded NU5125;SA0001 - portable - true - false From 669ac2c3064791ae38fc8b4bba8382d7a328c6fb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:10:16 -0800 Subject: [PATCH 5/6] add e2e tests --- .../PolymorphicActivityInput.cs | 149 ++++++++++++++++++ test/e2e/Apps/BasicDotNetIsolated/Program.cs | 19 +++ .../Tests/PolymorphicActivityInputTests.cs | 75 +++++++++ 3 files changed, 243 insertions(+) create mode 100644 test/e2e/Apps/BasicDotNetIsolated/PolymorphicActivityInput.cs create mode 100644 test/e2e/Tests/Tests/PolymorphicActivityInputTests.cs diff --git a/test/e2e/Apps/BasicDotNetIsolated/PolymorphicActivityInput.cs b/test/e2e/Apps/BasicDotNetIsolated/PolymorphicActivityInput.cs new file mode 100644 index 000000000..4cd4b2d57 --- /dev/null +++ b/test/e2e/Apps/BasicDotNetIsolated/PolymorphicActivityInput.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json.Serialization; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; + +namespace Microsoft.Azure.Durable.Tests.E2E; + +#region Polymorphic Model Classes + +[JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")] +[JsonDerivedType(typeof(Dog), "Dog")] +[JsonDerivedType(typeof(Cat), "Cat")] +[JsonDerivedType(typeof(Bird), "Bird")] +public class Animal +{ + public string? Name { get; set; } + public int Age { get; set; } +} + +public class Dog : Animal +{ + public string? Breed { get; set; } + public bool IsServiceDog { get; set; } +} + +public class Cat : Animal +{ + public int Lives { get; set; } + public bool IsIndoor { get; set; } +} + +public class Bird : Animal +{ + public double WingSpanInches { get; set; } + public bool CanFly { get; set; } +} + +#endregion + +public static class PolymorphicActivityInput +{ + /// + /// Orchestrator that tests polymorphic activity inputs. + /// This orchestration calls activities with different derived types and verifies + /// that the type information is preserved through serialization/deserialization. + /// + [Function(nameof(PolymorphicActivityInputOrchestrator))] + public static async Task> PolymorphicActivityInputOrchestrator( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + var output = new List(); + + // Test 1: Pass a Dog as Animal + var dog = new Dog + { + Name = "Max", + Age = 5, + Breed = "Golden Retriever", + IsServiceDog = true + }; + string dogResult = await context.CallActivityAsync(nameof(AnimalActivity), dog); + output.Add(dogResult); + + // Test 2: Pass a Cat as Animal + var cat = new Cat + { + Name = "Whiskers", + Age = 3, + Lives = 9, + IsIndoor = true + }; + string catResult = await context.CallActivityAsync(nameof(AnimalActivity), cat); + output.Add(catResult); + + // Test 3: Pass a Bird as Animal + var bird = new Bird + { + Name = "Tweety", + Age = 2, + WingSpanInches = 8.5, + CanFly = true + }; + string birdResult = await context.CallActivityAsync(nameof(AnimalActivity), bird); + output.Add(birdResult); + + // Test 4: Pass array of different animals (object[] internally) + Animal[] animals = new Animal[] { dog, cat, bird }; + string arrayResult = await context.CallActivityAsync(nameof(AnimalArrayActivity), animals); + output.Add(arrayResult); + + // Test 5: Pass a specific Dog type + string dogSpecificResult = await context.CallActivityAsync(nameof(DogActivity), dog); + output.Add(dogSpecificResult); + + return output; + } + + /// + /// Activity that receives Animal and uses pattern matching to determine actual type. + /// This verifies that polymorphic deserialization works correctly. + /// + [Function(nameof(AnimalActivity))] + public static string AnimalActivity([ActivityTrigger] Animal animal, FunctionContext executionContext) + { + return animal switch + { + Dog dog => $"Dog[{dog.Name}|{dog.Age}y|{dog.Breed}|ServiceDog={dog.IsServiceDog}]", + Cat cat => $"Cat[{cat.Name}|{cat.Age}y|Lives={cat.Lives}|Indoor={cat.IsIndoor}]", + Bird bird => $"Bird[{bird.Name}|{bird.Age}y|WingSpan={bird.WingSpanInches}in|CanFly={bird.CanFly}]", + _ => $"UnknownAnimal[{animal.Name}|{animal.Age}y|Type={animal.GetType().Name}]" + }; + } + + /// + /// Activity that receives an array of animals. + /// Tests that object[] arrays preserve polymorphic type information for each element. + /// + [Function(nameof(AnimalArrayActivity))] + public static string AnimalArrayActivity([ActivityTrigger] Animal[] animals, FunctionContext executionContext) + { + var results = new List(); + + foreach (var animal in animals) + { + string result = animal switch + { + Dog dog => $"Dog[{dog.Name}]", + Cat cat => $"Cat[{cat.Name}]", + Bird bird => $"Bird[{bird.Name}]", + _ => $"Unknown[{animal.Name}]" + }; + results.Add(result); + } + + return $"Array[{string.Join(", ", results)}]"; + } + + /// + /// Activity that receives a specific Dog type (not just Animal). + /// Tests that concrete type deserialization also works. + /// + [Function(nameof(DogActivity))] + public static string DogActivity([ActivityTrigger] Dog dog, FunctionContext executionContext) + { + return $"SpecificDog[{dog.Name}|{dog.Breed}|ServiceDog={dog.IsServiceDog}]"; + } +} \ No newline at end of file diff --git a/test/e2e/Apps/BasicDotNetIsolated/Program.cs b/test/e2e/Apps/BasicDotNetIsolated/Program.cs index 0a8fa5ca9..7e4381417 100644 --- a/test/e2e/Apps/BasicDotNetIsolated/Program.cs +++ b/test/e2e/Apps/BasicDotNetIsolated/Program.cs @@ -6,6 +6,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.DurableTask.Worker; using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Azure.Core.Serialization; var host = new HostBuilder() .ConfigureFunctionsWebApplication() @@ -18,6 +22,21 @@ // Register the custom exception properties provider services.AddSingleton(); + + // Configure JSON serialization to support polymorphic types + // This is required for E2E tests that verify polymorphic deserialization + services.Configure(options => + { + var jsonOptions = new JsonSerializerOptions + { + WriteIndented = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), // Recognizes [JsonPolymorphic] attributes + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.Never + }; + + options.Serializer = new JsonObjectSerializer(jsonOptions); + }); }) .Build(); diff --git a/test/e2e/Tests/Tests/PolymorphicActivityInputTests.cs b/test/e2e/Tests/Tests/PolymorphicActivityInputTests.cs new file mode 100644 index 000000000..7c294db4c --- /dev/null +++ b/test/e2e/Tests/Tests/PolymorphicActivityInputTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E; + +/// +/// E2E tests for polymorphic activity input deserialization in .NET Isolated Workers. +/// These tests verify that: +/// 1. Polymorphic types passed to activities are correctly deserialized to their derived types +/// 2. Pattern matching in activities works correctly with polymorphic inputs +/// 3. Object[] arrays preserve type information for each polymorphic element +/// 4. The ObjectConverterShim correctly handles the __type discriminator +/// +[Collection(Constants.FunctionAppCollectionName)] +public class PolymorphicActivityInputTests +{ + private readonly FunctionAppFixture fixture; + private readonly ITestOutputHelper output; + + public PolymorphicActivityInputTests(FunctionAppFixture fixture, ITestOutputHelper testOutputHelper) + { + this.fixture = fixture; + this.fixture.TestLogs.UseTestLogger(testOutputHelper); + this.output = testOutputHelper; + } + + /// + /// Tests that polymorphic activity inputs are correctly deserialized to their derived types. + /// This is the core E2E test for the polymorphic serialization feature. + /// + [Fact] + public async Task PolymorphicActivityInputs_DeserializeToCorrectDerivedTypes() + { + // Start the orchestration + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger( + "StartOrchestration", + "?orchestrationName=PolymorphicActivityInputOrchestrator"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); + + // Wait for orchestration to complete + await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Completed", 30); + + var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri); + + this.output.WriteLine($"Orchestration Output: {orchestrationDetails.Output}"); + + // Verify Dog was deserialized correctly with all properties + // Pattern matching in the activity should detect it as Dog type + Assert.Contains("Dog[Max|5y|Golden Retriever|ServiceDog=True]", orchestrationDetails.Output); + + // Verify Cat was deserialized correctly with all properties + Assert.Contains("Cat[Whiskers|3y|Lives=9|Indoor=True]", orchestrationDetails.Output); + + // Verify Bird was deserialized correctly with all properties + Assert.Contains("Bird[Tweety|2y|WingSpan=8.5in|CanFly=True]", orchestrationDetails.Output); + + // Verify array of polymorphic types preserves all type information + // This tests that object[] serialization correctly handles polymorphic elements + Assert.Contains("Array[Dog[Max], Cat[Whiskers], Bird[Tweety]]", orchestrationDetails.Output); + + // Verify specific Dog type deserialization + Assert.Contains("SpecificDog[Max|Golden Retriever|ServiceDog=True]", orchestrationDetails.Output); + + // Verify no "Unknown" types were encountered (which would indicate deserialization failure) + Assert.DoesNotContain("UnknownAnimal", orchestrationDetails.Output); + Assert.DoesNotContain("Unknown[", orchestrationDetails.Output); + } +} + From bc543f142ce0a9d0094236fce4935be8552a9fa9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:13:32 -0800 Subject: [PATCH 6/6] update release note --- release_notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release_notes.md b/release_notes.md index a39623b2f..75b79eeaa 100644 --- a/release_notes.md +++ b/release_notes.md @@ -18,6 +18,8 @@ ### Bug Fixes +- Support Polymorphic input payload deserialization (https://github.com/Azure/azure-functions-durable-extension/pull/3250) + ### Breaking Changes ### Dependency Updates