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
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