-
Notifications
You must be signed in to change notification settings - Fork 273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[out-of-proc] GRPC serializer does not respect Activity return type when [JsonPolymorphic]
is used
#3013
Comments
Does this problem exist for both activity and orchestration triggers or just activity triggers? Activity triggers go down a different serialization path owned by the .NET worker, whereas orchestration triggers use a serialization mechanism that's specific to Durable Functions. Knowing which scenarios are impacted will help us know where to focus efforts on investigating the root cause. /cc @jviau |
It is quite easy to check if I modify my example like so: using System.Text.Json.Serialization;
using Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(builder => builder.Serializer = new CustomSerializer())
.Build();
await host.RunAsync();
[JsonPolymorphic]
[JsonDerivedType(typeof(DerivedResponse), nameof(DerivedResponse))]
public abstract record BaseResponse(int Field1);
public sealed record DerivedResponse(int Field1, int Field2) : BaseResponse(Field1);
public static class DurableFunctions
{
[Function(nameof(TestActivity))]
public static Task<BaseResponse> TestActivity([ActivityTrigger] object? input)
{
return Task.FromResult<BaseResponse>(new DerivedResponse(42, 99));
}
[Function(nameof(TestAnotherOrchestration))]
public static Task<BaseResponse> TestAnotherOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
return Task.FromResult<BaseResponse>(new DerivedResponse(42, 99));
}
[Function(nameof(TestOrchestration))]
public static async Task TestOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
var logger = context.CreateReplaySafeLogger(nameof(TestOrchestration));
var result = await context.CallSubOrchestratorAsync<BaseResponse>(nameof(TestAnotherOrchestration));
logger.LogInformation("Received {Result}", result);
}
[Function(nameof(HttpEntryPoint))]
public static async Task HttpEntryPoint([HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "debug")] HttpRequestData req, [DurableClient] DurableTaskClient client)
{
await client.ScheduleNewOrchestrationInstanceAsync(nameof(TestOrchestration));
}
}
public sealed class CustomSerializer : JsonObjectSerializer
{
// Called from Microsoft.Azure.Functions.Worker.Extensions.DurableTask.ObjectConverterSlim.Serialize
public override BinaryData Serialize(object? value, Type? inputType = null, CancellationToken cancellationToken = default)
{
// Breakpoint here will reveal the issue
var result = base.Serialize(value, inputType, cancellationToken);
return result;
}
} Funny thing! Same issue but slightly different root cause, this time it is public override string? Serialize(object? value)
{
if (value is null)
{
return null;
}
BinaryData data = this.serializer.Serialize(value, value.GetType(), default);
return data.ToString();
} which again uses runtime type. So both do not work, conceptually the problem is the same, but implementation differs. |
I have a vague recollection of encountering something like this myself. I think it has to do with the API used on @Ilia-Kosenkov seeing as the issue lies in the Azure.Core's serializer, have you opened an issue or see if there is an existing one there? https://github.com/azure/azure-sdk-for-net/ |
Hey @jviau , thanks for the reply. If this is by-design, it would be nice to document it explicitly (unless it is already and we just missed it). Unfortunately, this is not the issue with As for |
@Ilia-Kosenkov ah I see. The fix would be to add a new overload @cgillum - this work seems valuable and fairly easy to do (shouldn't require any breaking changes). Not sure how you want to prioritize it. |
We did not debug deep enough into the framework, but assuming that at the moment of calling |
Looked into it a bit more. While adding the overload is trivial, all the call sites for If done via generics, this has a side benefit of avoiding boxing. |
Description
If activity is defined to return a base type of some hierarchy that supports polymorphic (de-)serialization, the actual serialization of activity output happens according to its runtime type rather than declared type, which breaks polymorphic feature of
System.Text.Json
.We were able to trace it to
Microsoft.Azure.Functions.Worker.Rpc.RpcExtensions.ToRpcDefault
, which callsserializer.Serialize(value)?.ToString();
(hereserializer
isJsonObjectSerializer
). This overload has the following signature:When called with one parameter,
inputType
is null. The implementation itself callsthus resolving runtime type of the return value, and serializing accordingly, instead of decalred type of activity.
Expected behavior
GRPC serializer passes return type as
inputType
, performing correct JSON serialization of the result.Actual behavior
Serialization happens according to runtime type, which breaks polymorphic serialization of
System.Text.Json
Relevant source code snippets
Program.cs
.csproj
Known workarounds
There is no reliable workaround because
ObjectSerializer
does not get type information or any other required information. If there are multiple functions that return both base and derived types, then universal solution is impossible.Half-solutions:
inputType
when it detects it is working with incorrect activity serialization output (introduces serialization penalty due to constant type checks and additional startup overhead)[JsonPolymorphic]
and build a custom converter, which defeats the purpose of relying on well-established frameworksApp Details
Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.2.2
AzureFunctionsVersion
isv4
)C#
Screenshots
Local state of
CustomSerializer
when activity return type is serialized. Notice thatinputType
isnull
, whiletypeof(BaseResponse)
is required her to function properly.The text was updated successfully, but these errors were encountered: