Skip to content

Support Polymorphic input payload deserialization (isolated dotnet)#3250

Open
YunchuWang wants to merge 9 commits intodevfrom
wangbill/fixpoly
Open

Support Polymorphic input payload deserialization (isolated dotnet)#3250
YunchuWang wants to merge 9 commits intodevfrom
wangbill/fixpoly

Conversation

@YunchuWang
Copy link
Member

@YunchuWang YunchuWang commented Nov 12, 2025

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

  • My changes do not require documentation changes
    • Otherwise: Documentation PR is ready to merge and referenced in pending_docs.md
  • My changes should not be added to the release notes for the next release
    • Otherwise: I've added my notes to release_notes.md
  • My changes do not need to be backported to a previous version
    • Otherwise: Backport tracked by issue/PR #issue_or_pr
  • I have added all required tests (Unit tests, E2E tests)
  • My changes do not require any extra work to be leveraged by OutOfProc SDKs
    • Otherwise: That work is being tracked here: #issue_or_pr_in_each_sdk
  • My changes do not change the version of the WebJobs.Extensions.DurableTask package
    • Otherwise: major or minor version updates are reflected in /src/Worker.Extensions.DurableTask/AssemblyInfo.cs
  • My changes do not add EventIds to our EventSource logs
    • Otherwise: Ensure the EventIds are within the supported range in our existing Windows infrastructure. You may validate this with a deployed app's telemetry. You may also extend the range by completing a PR such as this one.
  • My changes should be added to v2.x branch.
    • Otherwise: This change applies exclusively to WebJobs.Extensions.DurableTask v3.x. It will be retained only in the dev and main branches and will not be merged into the v2.x branch.

@YunchuWang YunchuWang changed the title Support Polymorphic input payload deserialization Support Polymorphic input payload deserialization (isolated dotnet) Nov 12, 2025
Copilot AI review requested due to automatic review settings December 30, 2025 19:42
Comment on lines +125 to +135
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());
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ObjectConverterShim to detect and handle polymorphic types by finding the JsonPolymorphicAttribute on 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;
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate using statement. using System.Diagnostics; is already declared on line 4. Remove this duplicate declaration.

Suggested change
using System.Diagnostics;

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +24
- Support Polymorphic input payload deserialization (https://github.com/Azure/azure-functions-durable-extension/pull/3250)

Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- Support Polymorphic input payload deserialization (https://github.com/Azure/azure-functions-durable-extension/pull/3250)

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +108
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;
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +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
{
/// <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
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +108
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;
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +135
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);
}
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Type serializeAs = GetPolymorphicBaseType(elementType) ?? elementType;

BinaryData elementData = this.serializer.Serialize(element, serializeAs, default);
jsonBuilder.Append(elementData.ToString());
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant call to 'ToString' on a String object.

Suggested change
jsonBuilder.Append(elementData.ToString());
jsonBuilder.Append(elementData);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Polymorphic deserialization of activity input fails (Isolated worker with Newtonsoft)

2 participants