Support Polymorphic input payload deserialization (isolated dotnet)#3250
Support Polymorphic input payload deserialization (isolated dotnet)#3250YunchuWang wants to merge 9 commits intodevfrom
Conversation
| 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); | ||
| } |
| Type serializeAs = GetPolymorphicBaseType(elementType) ?? elementType; | ||
|
|
||
| BinaryData elementData = this.serializer.Serialize(element, serializeAs, default); | ||
| jsonBuilder.Append(elementData.ToString()); |
There was a problem hiding this comment.
Pull request overview
This PR adds support for polymorphic input payload deserialization in the Durable Functions isolated .NET worker. The implementation enables activity functions to receive polymorphic types (derived classes) and correctly deserialize them using type discriminators, addressing scenarios where activities need to work with base class or interface-typed parameters that are actually concrete derived types at runtime.
Key Changes
- Modified
ObjectConverterShimto detect and handle polymorphic types by finding theJsonPolymorphicAttributeon base classes and serializing with the appropriate base type to include type discriminators - Added special handling for
object[]arrays to preserve polymorphic type information for each element - Added comprehensive unit tests and E2E tests demonstrating polymorphic deserialization with class inheritance
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/Worker.Extensions.DurableTask/ObjectConverterShim.cs |
Core implementation: added GetPolymorphicBaseType() method to find polymorphic base types and special object[] array handling to preserve type discriminators |
test/Worker.Extensions.DurableTask.Tests/ObjectConverterShimTests.cs |
Added comprehensive unit tests for serialization/deserialization of polymorphic types, arrays, and round-trip scenarios |
test/e2e/Apps/BasicDotNetIsolated/PolymorphicActivityInput.cs |
Added E2E test orchestration and activities demonstrating polymorphic Animal types (Dog, Cat, Bird) |
test/e2e/Tests/Tests/PolymorphicActivityInputTests.cs |
Added E2E test that verifies polymorphic types are correctly deserialized in activities |
test/e2e/Apps/BasicDotNetIsolated/Program.cs |
Configured System.Text.Json serializer with DefaultJsonTypeInfoResolver to support polymorphic attributes |
release_notes.md |
Added bug fix entry for polymorphic deserialization support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| using Microsoft.Azure.Functions.Worker; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.DurableTask.Worker; | ||
| using System.Diagnostics; |
There was a problem hiding this comment.
Duplicate using statement. using System.Diagnostics; is already declared on line 4. Remove this duplicate declaration.
| using System.Diagnostics; |
| - Support Polymorphic input payload deserialization (https://github.com/Azure/azure-functions-durable-extension/pull/3250) | ||
|
|
There was a problem hiding this comment.
The PR description checklist indicates that "My changes should not be added to the release notes for the next release" is checked, suggesting this change should not appear in the release notes. However, a release note entry has been added. Either the release note should be removed, or the PR description checklist should be updated to reflect that release notes are needed.
| - Support Polymorphic input payload deserialization (https://github.com/Azure/azure-functions-durable-extension/pull/3250) | |
| private static Type? GetPolymorphicBaseType(Type concreteType) | ||
| { | ||
| Type? current = concreteType.BaseType; | ||
| while (current != null && current != typeof(object)) | ||
| { | ||
| if (current.GetCustomAttribute<JsonPolymorphicAttribute>() != null) | ||
| { | ||
| return current; | ||
| } | ||
| current = current.BaseType; | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The GetPolymorphicBaseType method only searches for JsonPolymorphicAttribute on base classes using the inheritance chain (BaseType property), but does not check implemented interfaces. According to issue #3182, users need to support polymorphic deserialization for interface-typed activity inputs (e.g., IVehicle). Consider also checking the type's implemented interfaces using GetInterfaces() to find interfaces decorated with JsonPolymorphicAttribute.
| // 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 | ||
| { | ||
| /// <summary> | ||
| /// 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. | ||
| /// </summary> | ||
| [Function(nameof(PolymorphicActivityInputOrchestrator))] | ||
| public static async Task<List<string>> PolymorphicActivityInputOrchestrator( | ||
| [OrchestrationTrigger] TaskOrchestrationContext context) | ||
| { | ||
| var output = new List<string>(); | ||
|
|
||
| // 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<string>(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<string>(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<string>(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<string>(nameof(AnimalArrayActivity), animals); | ||
| output.Add(arrayResult); | ||
|
|
||
| // Test 5: Pass a specific Dog type | ||
| string dogSpecificResult = await context.CallActivityAsync<string>(nameof(DogActivity), dog); | ||
| output.Add(dogSpecificResult); | ||
|
|
||
| return output; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Activity that receives Animal and uses pattern matching to determine actual type. | ||
| /// This verifies that polymorphic deserialization works correctly. | ||
| /// </summary> | ||
| [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}]" | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Activity that receives an array of animals. | ||
| /// Tests that object[] arrays preserve polymorphic type information for each element. | ||
| /// </summary> | ||
| [Function(nameof(AnimalArrayActivity))] | ||
| public static string AnimalArrayActivity([ActivityTrigger] Animal[] animals, FunctionContext executionContext) | ||
| { | ||
| var results = new List<string>(); | ||
|
|
||
| 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)}]"; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Activity that receives a specific Dog type (not just Animal). | ||
| /// Tests that concrete type deserialization also works. | ||
| /// </summary> | ||
| [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 |
There was a problem hiding this comment.
The tests only cover class inheritance-based polymorphism (Animal base class with Dog, Cat, Bird derived classes), but the original issue #3182 specifically mentions interface-based polymorphism scenarios (e.g., IVehicle interface with Car implementation). Consider adding test cases that verify polymorphic deserialization works for interface-typed activity inputs.
| private static Type? GetPolymorphicBaseType(Type concreteType) | ||
| { | ||
| Type? current = concreteType.BaseType; | ||
| while (current != null && current != typeof(object)) | ||
| { | ||
| if (current.GetCustomAttribute<JsonPolymorphicAttribute>() != null) | ||
| { | ||
| return current; | ||
| } | ||
| current = current.BaseType; | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The implementation only supports System.Text.Json's JsonPolymorphicAttribute, but the original issue #3182 specifically mentions using Newtonsoft.Json with TypeNameHandling.All. The current implementation will not detect polymorphic types when using Newtonsoft.Json, as it won't find the attribute. Consider either documenting that this feature only works with System.Text.Json, or extend the implementation to also support Newtonsoft.Json's type handling mechanisms (e.g., checking for $type property in the serialized JSON).
| 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); | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| Type serializeAs = GetPolymorphicBaseType(elementType) ?? elementType; | ||
|
|
||
| BinaryData elementData = this.serializer.Serialize(element, serializeAs, default); | ||
| jsonBuilder.Append(elementData.ToString()); |
There was a problem hiding this comment.
Redundant call to 'ToString' on a String object.
| jsonBuilder.Append(elementData.ToString()); | |
| jsonBuilder.Append(elementData); |
Fix #3182
This PR enables polymorphic deserialization of input payloads in the Durable Functions extension. In practical terms, it allows the system to accept inputs where the payload type isn’t known at compile time (or is one of several derived types) and properly deserialize them into the right concrete types rather than always using a base type or failing.
🧠 What the change addresses
Prior issue: Activities (especially in the isolated worker model) using e.g. Newtonsoft.Json or other serializers could not reliably handle input JSON that contained a type discriminator or represented a derived type of the declared parameter. This prevented polymorphic scenarios.
Issue describing the changes in this PR
resolves #issue_for_this_pr
Pull request checklist
pending_docs.mdrelease_notes.md/src/Worker.Extensions.DurableTask/AssemblyInfo.csdevandmainbranches and will not be merged into thev2.xbranch.