Skip to content

Commit 1d2ca4c

Browse files
brunolins16halter73eiriktsarpalis
authored
Changing to use the non-generic WriteAsJsonAsync (#43960)
* Changing to use the non-generic WriteAsJsonAsync * Update src/Http/Http.Extensions/src/RequestDelegateFactory.cs Co-authored-by: Stephen Halter <[email protected]> * Update src/Http/Http.Extensions/src/RequestDelegateFactory.cs Co-authored-by: Eirik Tsarpalis <[email protected]> Co-authored-by: Stephen Halter <[email protected]> Co-authored-by: Eirik Tsarpalis <[email protected]>
1 parent b146c90 commit 1d2ca4c

File tree

4 files changed

+224
-20
lines changed

4 files changed

+224
-20
lines changed

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@ public static partial class RequestDelegateFactory
6262
private static readonly PropertyInfo HeaderIndexerProperty = typeof(IHeaderDictionary).GetProperty("Item")!;
6363
private static readonly PropertyInfo FormFilesIndexerProperty = typeof(IFormFileCollection).GetProperty("Item")!;
6464

65-
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
66-
// https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
67-
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object?, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync<object?>(response, value, default));
65+
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WriteJsonResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
6866

6967
private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo<Action<HttpContext, string, string, string, bool>>((httpContext, parameterType, parameterName, sourceValue, shouldThrow) =>
7068
Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue, shouldThrow));
@@ -1086,11 +1084,11 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
10861084
else if (returnType.IsValueType)
10871085
{
10881086
var box = Expression.TypeAs(methodCall, typeof(object));
1089-
return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None));
1087+
return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box);
10901088
}
10911089
else
10921090
{
1093-
return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None));
1091+
return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall);
10941092
}
10951093
}
10961094

@@ -2036,8 +2034,7 @@ private static Task ExecuteAwaitedReturn(object obj, HttpContext httpContext)
20362034
else
20372035
{
20382036
// Otherwise, we JSON serialize when we reach the terminal state
2039-
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
2040-
return httpContext.Response.WriteAsJsonAsync<object?>(obj);
2037+
return WriteJsonResponse(httpContext.Response, obj);
20412038
}
20422039
}
20432040

@@ -2047,14 +2044,12 @@ private static Task ExecuteTaskOfT<T>(Task<T> task, HttpContext httpContext)
20472044

20482045
static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext)
20492046
{
2050-
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
2051-
await httpContext.Response.WriteAsJsonAsync<object?>(await task);
2047+
await WriteJsonResponse(httpContext.Response, await task);
20522048
}
20532049

20542050
if (task.IsCompletedSuccessfully)
20552051
{
2056-
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
2057-
return httpContext.Response.WriteAsJsonAsync<object?>(task.GetAwaiter().GetResult());
2052+
return WriteJsonResponse(httpContext.Response, task.GetAwaiter().GetResult());
20582053
}
20592054

20602055
return ExecuteAwaited(task, httpContext);
@@ -2137,14 +2132,12 @@ private static Task ExecuteValueTaskOfT<T>(ValueTask<T> task, HttpContext httpCo
21372132
{
21382133
static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext)
21392134
{
2140-
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
2141-
await httpContext.Response.WriteAsJsonAsync<object?>(await task);
2135+
await WriteJsonResponse(httpContext.Response, await task);
21422136
}
21432137

21442138
if (task.IsCompletedSuccessfully)
21452139
{
2146-
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
2147-
return httpContext.Response.WriteAsJsonAsync<object?>(task.GetAwaiter().GetResult());
2140+
return WriteJsonResponse(httpContext.Response, task.GetAwaiter().GetResult());
21482141
}
21492142

21502143
return ExecuteAwaited(task, httpContext);
@@ -2194,6 +2187,16 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex
21942187
await EnsureRequestResultNotNull(result).ExecuteAsync(httpContext);
21952188
}
21962189

2190+
private static Task WriteJsonResponse(HttpResponse response, object? value)
2191+
{
2192+
// Call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type
2193+
// and avoid source generators issues.
2194+
// https://github.com/dotnet/aspnetcore/issues/43894
2195+
// https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
2196+
return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, value is null ? typeof(object) : value.GetType(), default);
2197+
2198+
}
2199+
21972200
private static NotSupportedException GetUnsupportedReturnTypeException(Type returnType)
21982201
{
21992202
return new NotSupportedException($"Unsupported return type: {TypeNameHelper.GetTypeDisplayName(returnType)}");

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

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
namespace Microsoft.AspNetCore.Routing.Internal;
3636

37-
public class RequestDelegateFactoryTests : LoggedTest
37+
public partial class RequestDelegateFactoryTests : LoggedTest
3838
{
3939
public static IEnumerable<object[]> NoResult
4040
{
@@ -3013,6 +3013,46 @@ public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody(D
30133013
Assert.Equal("With type hierarchies!", deserializedResponseBody!.Child);
30143014
}
30153015

3016+
public static IEnumerable<object[]> JsonContextActions
3017+
{
3018+
get
3019+
{
3020+
return ComplexResult.Concat(ChildResult);
3021+
}
3022+
}
3023+
3024+
[JsonSerializable(typeof(Todo))]
3025+
[JsonSerializable(typeof(TodoChild))]
3026+
private partial class TestJsonContext : JsonSerializerContext
3027+
{ }
3028+
3029+
[Theory]
3030+
[MemberData(nameof(JsonContextActions))]
3031+
public async Task RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerContext(Delegate @delegate)
3032+
{
3033+
var httpContext = CreateHttpContext();
3034+
httpContext.RequestServices = new ServiceCollection()
3035+
.AddSingleton(LoggerFactory)
3036+
.ConfigureHttpJsonOptions(o => o.SerializerOptions.AddContext<TestJsonContext>())
3037+
.BuildServiceProvider();
3038+
3039+
var responseBodyStream = new MemoryStream();
3040+
httpContext.Response.Body = responseBodyStream;
3041+
3042+
var factoryResult = RequestDelegateFactory.Create(@delegate);
3043+
var requestDelegate = factoryResult.RequestDelegate;
3044+
3045+
await requestDelegate(httpContext);
3046+
3047+
var deserializedResponseBody = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), new JsonSerializerOptions
3048+
{
3049+
PropertyNameCaseInsensitive = true
3050+
});
3051+
3052+
Assert.NotNull(deserializedResponseBody);
3053+
Assert.Equal("Write even more tests!", deserializedResponseBody!.Name);
3054+
}
3055+
30163056
public static IEnumerable<object[]> CustomResults
30173057
{
30183058
get

src/Http/Http.Results/src/HttpResultsHelper.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,10 @@ public static Task WriteResultAsJsonAsync<T>(
2828
}
2929

3030
var declaredType = typeof(T);
31-
32-
Log.WritingResultAsJson(logger, declaredType.Name);
33-
3431
if (declaredType.IsValueType)
3532
{
33+
Log.WritingResultAsJson(logger, declaredType.Name);
34+
3635
// In this case the polymorphism is not
3736
// relevant and we don't need to box.
3837
return httpContext.Response.WriteAsJsonAsync(
@@ -41,8 +40,17 @@ public static Task WriteResultAsJsonAsync<T>(
4140
contentType: contentType);
4241
}
4342

44-
return httpContext.Response.WriteAsJsonAsync<object?>(
43+
var runtimeType = value.GetType();
44+
45+
Log.WritingResultAsJson(logger, runtimeType.Name);
46+
47+
// Call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type
48+
// and avoid source generators issues.
49+
// https://github.com/dotnet/aspnetcore/issues/43894
50+
// https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
51+
return httpContext.Response.WriteAsJsonAsync(
4552
value,
53+
runtimeType,
4654
options: jsonSerializerOptions,
4755
contentType: contentType);
4856
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
10+
namespace Microsoft.AspNetCore.Http.HttpResults;
11+
12+
public partial class HttpResultsHelperTests
13+
{
14+
[Theory]
15+
[InlineData(true)]
16+
[InlineData(false)]
17+
public async Task WriteResultAsJsonAsync_Works_ForValueTypes(bool useJsonContext)
18+
{
19+
// Arrange
20+
var value = new TodoStruct()
21+
{
22+
Id = 1,
23+
IsComplete = true,
24+
Name = "Write even more tests!",
25+
};
26+
var responseBodyStream = new MemoryStream();
27+
var httpContext = CreateHttpContext(responseBodyStream, useJsonContext);
28+
29+
// Act
30+
await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value);
31+
32+
// Assert
33+
var body = JsonSerializer.Deserialize<TodoStruct>(responseBodyStream.ToArray(), new JsonSerializerOptions
34+
{
35+
PropertyNameCaseInsensitive = true
36+
});
37+
38+
Assert.Equal("Write even more tests!", body!.Name);
39+
Assert.True(body!.IsComplete);
40+
}
41+
42+
[Theory]
43+
[InlineData(true)]
44+
[InlineData(false)]
45+
public async Task WriteResultAsJsonAsync_Works_ForReferenceTypes(bool useJsonContext)
46+
{
47+
// Arrange
48+
var value = new Todo()
49+
{
50+
Id = 1,
51+
IsComplete = true,
52+
Name = "Write even more tests!",
53+
};
54+
var responseBodyStream = new MemoryStream();
55+
var httpContext = CreateHttpContext(responseBodyStream, useJsonContext);
56+
57+
// Act
58+
await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value);
59+
60+
// Assert
61+
var body = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), new JsonSerializerOptions
62+
{
63+
PropertyNameCaseInsensitive = true
64+
});
65+
66+
Assert.NotNull(body);
67+
Assert.Equal("Write even more tests!", body!.Name);
68+
Assert.True(body!.IsComplete);
69+
}
70+
71+
[Theory]
72+
[InlineData(true)]
73+
[InlineData(false)]
74+
public async Task WriteResultAsJsonAsync_Works_ForChildTypes(bool useJsonContext)
75+
{
76+
// Arrange
77+
var value = new TodoChild()
78+
{
79+
Id = 1,
80+
IsComplete = true,
81+
Name = "Write even more tests!",
82+
Child = "With type hierarchies!"
83+
};
84+
var responseBodyStream = new MemoryStream();
85+
var httpContext = CreateHttpContext(responseBodyStream, useJsonContext);
86+
87+
// Act
88+
await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value);
89+
90+
// Assert
91+
var body = JsonSerializer.Deserialize<TodoChild>(responseBodyStream.ToArray(), new JsonSerializerOptions
92+
{
93+
PropertyNameCaseInsensitive = true
94+
});
95+
96+
Assert.NotNull(body);
97+
Assert.Equal("Write even more tests!", body!.Name);
98+
Assert.True(body!.IsComplete);
99+
Assert.Equal("With type hierarchies!", body!.Child);
100+
}
101+
102+
private static DefaultHttpContext CreateHttpContext(Stream stream, bool useJsonContext = false)
103+
=> new()
104+
{
105+
RequestServices = CreateServices(useJsonContext),
106+
Response =
107+
{
108+
Body = stream,
109+
},
110+
};
111+
112+
private static IServiceProvider CreateServices(bool useJsonContext = false)
113+
{
114+
var services = new ServiceCollection();
115+
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
116+
117+
if (useJsonContext)
118+
{
119+
services.ConfigureHttpJsonOptions(o => o.SerializerOptions.AddContext<TestJsonContext>());
120+
}
121+
122+
return services.BuildServiceProvider();
123+
}
124+
125+
[JsonSerializable(typeof(Todo))]
126+
[JsonSerializable(typeof(TodoChild))]
127+
[JsonSerializable(typeof(TodoStruct))]
128+
private partial class TestJsonContext : JsonSerializerContext
129+
{ }
130+
131+
private class Todo
132+
{
133+
public int Id { get; set; }
134+
public string Name { get; set; } = "Todo";
135+
public bool IsComplete { get; set; }
136+
}
137+
138+
private struct TodoStruct
139+
{
140+
public TodoStruct()
141+
{
142+
}
143+
144+
public int Id { get; set; }
145+
public string Name { get; set; } = "Todo";
146+
public bool IsComplete { get; set; }
147+
}
148+
149+
private class TodoChild : Todo
150+
{
151+
public string Child { get; set; }
152+
}
153+
}

0 commit comments

Comments
 (0)