Browse Source

Consistently serialize child members returned from route handlers (#39858)

Stephen Halter 4 years ago
parent
commit
088595a349

+ 14 - 6
src/Http/Http.Extensions/src/RequestDelegateFactory.cs

@@ -38,9 +38,12 @@ public static partial class RequestDelegateFactory
     private static readonly MethodInfo GetServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
     private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
     private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
-    private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default));
     private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!;
 
+    // Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
+    // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
+    private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object?, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync<object?>(response, value, default));
+
     private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo<Action<HttpContext, string, string, string, bool>>((httpContext, parameterType, parameterName, sourceValue, shouldThrow) =>
         Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue, shouldThrow));
     private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo<Action<HttpContext, string, string, string, bool>>((httpContext, parameterType, parameterName, source, shouldThrow) =>
@@ -1442,7 +1445,8 @@ public static partial class RequestDelegateFactory
         else
         {
             // Otherwise, we JSON serialize when we reach the terminal state
-            await httpContext.Response.WriteAsJsonAsync(obj);
+            // Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
+            await httpContext.Response.WriteAsJsonAsync<object?>(obj);
         }
     }
 
@@ -1452,12 +1456,14 @@ public static partial class RequestDelegateFactory
 
         static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext)
         {
-            await httpContext.Response.WriteAsJsonAsync(await task);
+            // Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
+            await httpContext.Response.WriteAsJsonAsync<object?>(await task);
         }
 
         if (task.IsCompletedSuccessfully)
         {
-            return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult());
+            // Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
+            return httpContext.Response.WriteAsJsonAsync<object?>(task.GetAwaiter().GetResult());
         }
 
         return ExecuteAwaited(task, httpContext);
@@ -1506,12 +1512,14 @@ public static partial class RequestDelegateFactory
     {
         static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext)
         {
-            await httpContext.Response.WriteAsJsonAsync(await task);
+            // Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
+            await httpContext.Response.WriteAsJsonAsync<object?>(await task);
         }
 
         if (task.IsCompletedSuccessfully)
         {
-            return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult());
+            // Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
+            return httpContext.Response.WriteAsJsonAsync<object?>(task.GetAwaiter().GetResult());
         }
 
         return ExecuteAwaited(task, httpContext);

+ 65 - 2
src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

@@ -2309,8 +2309,6 @@ public class RequestDelegateFactoryTests : LoggedTest
 
         var deserializedResponseBody = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), new JsonSerializerOptions
         {
-            // TODO: the output is "{\"id\":0,\"name\":\"Write even more tests!\",\"isComplete\":false}"
-            // Verify that the camelCased property names are consistent with MVC and if so whether we should keep the behavior.
             PropertyNameCaseInsensitive = true
         });
 
@@ -2318,6 +2316,66 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal("Write even more tests!", deserializedResponseBody!.Name);
     }
 
+    public static IEnumerable<object[]> ChildResult
+    {
+        get
+        {
+            TodoChild originalTodo = new()
+            {
+                Name = "Write even more tests!",
+                Child = "With type hierarchies!",
+            };
+
+            Todo TestAction() => originalTodo;
+
+            Task<Todo> TaskTestAction() => Task.FromResult<Todo>(originalTodo);
+            async Task<Todo> TaskTestActionAwaited()
+            {
+                await Task.Yield();
+                return originalTodo;
+            }
+
+            ValueTask<Todo> ValueTaskTestAction() => ValueTask.FromResult<Todo>(originalTodo);
+            async ValueTask<Todo> ValueTaskTestActionAwaited()
+            {
+                await Task.Yield();
+                return originalTodo;
+            }
+
+            return new List<object[]>
+                {
+                    new object[] { (Func<Todo>)TestAction },
+                    new object[] { (Func<Task<Todo>>)TaskTestAction},
+                    new object[] { (Func<Task<Todo>>)TaskTestActionAwaited},
+                    new object[] { (Func<ValueTask<Todo>>)ValueTaskTestAction},
+                    new object[] { (Func<ValueTask<Todo>>)ValueTaskTestActionAwaited},
+                };
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(ChildResult))]
+    public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody(Delegate @delegate)
+    {
+        var httpContext = CreateHttpContext();
+        var responseBodyStream = new MemoryStream();
+        httpContext.Response.Body = responseBodyStream;
+
+        var factoryResult = RequestDelegateFactory.Create(@delegate);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        var deserializedResponseBody = JsonSerializer.Deserialize<TodoChild>(responseBodyStream.ToArray(), new JsonSerializerOptions
+        {
+            PropertyNameCaseInsensitive = true
+        });
+
+        Assert.NotNull(deserializedResponseBody);
+        Assert.Equal("Write even more tests!", deserializedResponseBody!.Name);
+        Assert.Equal("With type hierarchies!", deserializedResponseBody!.Child);
+    }
+
     public static IEnumerable<object[]> CustomResults
     {
         get
@@ -4165,6 +4223,11 @@ public class RequestDelegateFactoryTests : LoggedTest
         public bool IsComplete { get; set; }
     }
 
+    private class TodoChild : Todo
+    {
+        public string? Child { get; set; }
+    }
+
     private class CustomTodo : Todo
     {
         public static async ValueTask<CustomTodo?> BindAsync(HttpContext context, ParameterInfo parameter)