Skip to content

Commit 088595a

Browse files
authored
Consistently serialize child members returned from route handlers (#39858)
1 parent 73ce504 commit 088595a

File tree

2 files changed

+79
-8
lines changed

2 files changed

+79
-8
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ public static partial class RequestDelegateFactory
3838
private static readonly MethodInfo GetServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
3939
private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
4040
private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
41-
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default));
4241
private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!;
4342

43+
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
44+
// https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
45+
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object?, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync<object?>(response, value, default));
46+
4447
private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo<Action<HttpContext, string, string, string, bool>>((httpContext, parameterType, parameterName, sourceValue, shouldThrow) =>
4548
Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue, shouldThrow));
4649
private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo<Action<HttpContext, string, string, string, bool>>((httpContext, parameterType, parameterName, source, shouldThrow) =>
@@ -1442,7 +1445,8 @@ private static async Task ExecuteObjectReturn(object? obj, HttpContext httpConte
14421445
else
14431446
{
14441447
// Otherwise, we JSON serialize when we reach the terminal state
1445-
await httpContext.Response.WriteAsJsonAsync(obj);
1448+
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
1449+
await httpContext.Response.WriteAsJsonAsync<object?>(obj);
14461450
}
14471451
}
14481452

@@ -1452,12 +1456,14 @@ private static Task ExecuteTask<T>(Task<T> task, HttpContext httpContext)
14521456

14531457
static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext)
14541458
{
1455-
await httpContext.Response.WriteAsJsonAsync(await task);
1459+
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
1460+
await httpContext.Response.WriteAsJsonAsync<object?>(await task);
14561461
}
14571462

14581463
if (task.IsCompletedSuccessfully)
14591464
{
1460-
return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult());
1465+
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
1466+
return httpContext.Response.WriteAsJsonAsync<object?>(task.GetAwaiter().GetResult());
14611467
}
14621468

14631469
return ExecuteAwaited(task, httpContext);
@@ -1506,12 +1512,14 @@ private static Task ExecuteValueTaskOfT<T>(ValueTask<T> task, HttpContext httpCo
15061512
{
15071513
static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext)
15081514
{
1509-
await httpContext.Response.WriteAsJsonAsync(await task);
1515+
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
1516+
await httpContext.Response.WriteAsJsonAsync<object?>(await task);
15101517
}
15111518

15121519
if (task.IsCompletedSuccessfully)
15131520
{
1514-
return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult());
1521+
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
1522+
return httpContext.Response.WriteAsJsonAsync<object?>(task.GetAwaiter().GetResult());
15151523
}
15161524

15171525
return ExecuteAwaited(task, httpContext);

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2309,15 +2309,73 @@ public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Dele
23092309

23102310
var deserializedResponseBody = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), new JsonSerializerOptions
23112311
{
2312-
// TODO: the output is "{\"id\":0,\"name\":\"Write even more tests!\",\"isComplete\":false}"
2313-
// Verify that the camelCased property names are consistent with MVC and if so whether we should keep the behavior.
23142312
PropertyNameCaseInsensitive = true
23152313
});
23162314

23172315
Assert.NotNull(deserializedResponseBody);
23182316
Assert.Equal("Write even more tests!", deserializedResponseBody!.Name);
23192317
}
23202318

2319+
public static IEnumerable<object[]> ChildResult
2320+
{
2321+
get
2322+
{
2323+
TodoChild originalTodo = new()
2324+
{
2325+
Name = "Write even more tests!",
2326+
Child = "With type hierarchies!",
2327+
};
2328+
2329+
Todo TestAction() => originalTodo;
2330+
2331+
Task<Todo> TaskTestAction() => Task.FromResult<Todo>(originalTodo);
2332+
async Task<Todo> TaskTestActionAwaited()
2333+
{
2334+
await Task.Yield();
2335+
return originalTodo;
2336+
}
2337+
2338+
ValueTask<Todo> ValueTaskTestAction() => ValueTask.FromResult<Todo>(originalTodo);
2339+
async ValueTask<Todo> ValueTaskTestActionAwaited()
2340+
{
2341+
await Task.Yield();
2342+
return originalTodo;
2343+
}
2344+
2345+
return new List<object[]>
2346+
{
2347+
new object[] { (Func<Todo>)TestAction },
2348+
new object[] { (Func<Task<Todo>>)TaskTestAction},
2349+
new object[] { (Func<Task<Todo>>)TaskTestActionAwaited},
2350+
new object[] { (Func<ValueTask<Todo>>)ValueTaskTestAction},
2351+
new object[] { (Func<ValueTask<Todo>>)ValueTaskTestActionAwaited},
2352+
};
2353+
}
2354+
}
2355+
2356+
[Theory]
2357+
[MemberData(nameof(ChildResult))]
2358+
public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody(Delegate @delegate)
2359+
{
2360+
var httpContext = CreateHttpContext();
2361+
var responseBodyStream = new MemoryStream();
2362+
httpContext.Response.Body = responseBodyStream;
2363+
2364+
var factoryResult = RequestDelegateFactory.Create(@delegate);
2365+
var requestDelegate = factoryResult.RequestDelegate;
2366+
2367+
await requestDelegate(httpContext);
2368+
2369+
var deserializedResponseBody = JsonSerializer.Deserialize<TodoChild>(responseBodyStream.ToArray(), new JsonSerializerOptions
2370+
{
2371+
PropertyNameCaseInsensitive = true
2372+
});
2373+
2374+
Assert.NotNull(deserializedResponseBody);
2375+
Assert.Equal("Write even more tests!", deserializedResponseBody!.Name);
2376+
Assert.Equal("With type hierarchies!", deserializedResponseBody!.Child);
2377+
}
2378+
23212379
public static IEnumerable<object[]> CustomResults
23222380
{
23232381
get
@@ -4165,6 +4223,11 @@ private class Todo : ITodo
41654223
public bool IsComplete { get; set; }
41664224
}
41674225

4226+
private class TodoChild : Todo
4227+
{
4228+
public string? Child { get; set; }
4229+
}
4230+
41684231
private class CustomTodo : Todo
41694232
{
41704233
public static async ValueTask<CustomTodo?> BindAsync(HttpContext context, ParameterInfo parameter)

0 commit comments

Comments
 (0)