Skip to content

Commit 08f6eea

Browse files
authored
Stop Calling JSON GetTypeInfo with the runtime type (#47859)
* Stop Calling JSON GetTypeInfo with the runtime type Calling GetTypeInfo with the object's runtime type doesn't work when using unspeakable types and using JSON source generation. There is no way to mark the unspeakable type (like a C# compiler implemented IAsyncEnumerable iterator) as JsonSerializable. Instead, when the "declared type"'s JsonTypeInfo isn't compatible with the runtime type w.r.t. polymorphism, serialize the value "as object", letting System.Text.Json's serialization take over to serialize the value. Fix #47548 * Move RequestDelegateFactory tests to be shared with RDG * Add justifications for suppressing the warnings from JsonSerializer. * Convert RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerContext to shared test between RDF and RDG. * Remove redundant JsonSerializerOptions. * Rename JsonTypeInfo extension method IsValid to ShouldUseWith.
1 parent 47d0784 commit 08f6eea

17 files changed

+321
-119
lines changed

src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,18 @@ private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string pa
149149
""";
150150

151151
public static string WriteToResponseAsyncMethod => """
152-
private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo, JsonSerializerOptions options)
152+
[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
153+
Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")]
154+
[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")]
155+
private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo)
153156
{
154157
var runtimeType = value?.GetType();
155158
if (runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.PolymorphismOptions is not null)
156159
{
157160
return httpContext.Response.WriteAsJsonAsync(value!, jsonTypeInfo);
158161
}
159162
160-
return httpContext.Response.WriteAsJsonAsync(value!, options.GetTypeInfo(runtimeType));
163+
return httpContext.Response.WriteAsJsonAsync<object?>(value, jsonTypeInfo.Options);
161164
}
162165
""";
163166

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointJsonResponseEmitter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ internal static string EmitJsonResponse(this EndpointResponse endpointResponse)
2323
{
2424
return $"httpContext.Response.WriteAsJsonAsync(result, jsonTypeInfo);";
2525
}
26-
return $"GeneratedRouteBuilderExtensionsCore.WriteToResponseAsync(result, httpContext, jsonTypeInfo, serializerOptions);";
26+
return $"GeneratedRouteBuilderExtensionsCore.WriteToResponseAsync(result, httpContext, jsonTypeInfo);";
2727
}
2828
}

src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ public static partial class HttpResponseJsonExtensions
3030
/// <param name="value">The value to write as JSON.</param>
3131
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
3232
/// <returns>The task object representing the asynchronous operation.</returns>
33+
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
34+
[RequiresDynamicCode(RequiresDynamicCodeMessage)]
3335
public static Task WriteAsJsonAsync<TValue>(
3436
this HttpResponse response,
3537
TValue value,
3638
CancellationToken cancellationToken = default)
3739
{
3840
ArgumentNullException.ThrowIfNull(response);
3941

40-
var options = ResolveSerializerOptions(response.HttpContext);
41-
return response.WriteAsJsonAsync(value, jsonTypeInfo: (JsonTypeInfo<TValue>)options.GetTypeInfo(typeof(TValue)), contentType: null, cancellationToken);
42+
return response.WriteAsJsonAsync(value, options: null, contentType: null, cancellationToken);
4243
}
4344

4445
/// <summary>
@@ -108,9 +109,7 @@ public static Task WriteAsJsonAsync<TValue>(
108109
/// <param name="contentType">The content-type to set on the response.</param>
109110
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
110111
/// <returns>The task object representing the asynchronous operation.</returns>
111-
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
112112
public static Task WriteAsJsonAsync<TValue>(
113-
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
114113
this HttpResponse response,
115114
TValue value,
116115
JsonTypeInfo<TValue> jsonTypeInfo,
@@ -204,6 +203,8 @@ private static async Task WriteAsJsonAsyncSlow<TValue>(
204203
/// <param name="type">The type of object to write.</param>
205204
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
206205
/// <returns>The task object representing the asynchronous operation.</returns>
206+
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
207+
[RequiresDynamicCode(RequiresDynamicCodeMessage)]
207208
public static Task WriteAsJsonAsync(
208209
this HttpResponse response,
209210
object? value,
@@ -212,8 +213,7 @@ public static Task WriteAsJsonAsync(
212213
{
213214
ArgumentNullException.ThrowIfNull(response);
214215

215-
var options = ResolveSerializerOptions(response.HttpContext);
216-
return response.WriteAsJsonAsync(value, jsonTypeInfo: options.GetTypeInfo(type), contentType: null, cancellationToken);
216+
return response.WriteAsJsonAsync(value, type, options: null, contentType: null, cancellationToken);
217217
}
218218

219219
/// <summary>
@@ -302,9 +302,7 @@ private static async Task WriteAsJsonAsyncSlow(
302302
/// <param name="contentType">The content-type to set on the response.</param>
303303
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
304304
/// <returns>The task object representing the asynchronous operation.</returns>
305-
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
306305
public static Task WriteAsJsonAsync(
307-
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
308306
this HttpResponse response,
309307
object? value,
310308
Type type,

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

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat
275275
EndpointBuilder = endpointBuilder,
276276
MetadataAlreadyInferred = metadataResult is not null,
277277
JsonSerializerOptions = jsonSerializerOptions,
278-
JsonSerializerOptionsExpression = Expression.Constant(jsonSerializerOptions, typeof(JsonSerializerOptions)),
279278
};
280279

281280
return factoryContext;
@@ -1000,23 +999,20 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
1000999
ExecuteAwaitedReturnMethod,
10011000
methodCall,
10021001
HttpContextExpr,
1003-
factoryContext.JsonSerializerOptionsExpression,
10041002
Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo<object>)));
10051003
}
10061004
else if (returnType == typeof(ValueTask<object>))
10071005
{
10081006
return Expression.Call(ExecuteValueTaskOfObjectMethod,
10091007
methodCall,
10101008
HttpContextExpr,
1011-
factoryContext.JsonSerializerOptionsExpression,
10121009
Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo<object>)));
10131010
}
10141011
else if (returnType == typeof(Task<object>))
10151012
{
10161013
return Expression.Call(ExecuteTaskOfObjectMethod,
10171014
methodCall,
10181015
HttpContextExpr,
1019-
factoryContext.JsonSerializerOptionsExpression,
10201016
Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo<object>)));
10211017
}
10221018
else if (AwaitableInfo.IsTypeAwaitable(returnType, out _))
@@ -1068,7 +1064,6 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
10681064
ExecuteTaskOfTMethod.MakeGenericMethod(typeArg),
10691065
methodCall,
10701066
HttpContextExpr,
1071-
factoryContext.JsonSerializerOptionsExpression,
10721067
Expression.Constant(jsonTypeInfo, typeof(JsonTypeInfo<>).MakeGenericType(typeArg)));
10731068
}
10741069
}
@@ -1109,7 +1104,6 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
11091104
ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg),
11101105
methodCall,
11111106
HttpContextExpr,
1112-
factoryContext.JsonSerializerOptionsExpression,
11131107
Expression.Constant(jsonTypeInfo, typeof(JsonTypeInfo<>).MakeGenericType(typeArg)));
11141108
}
11151109
}
@@ -1154,7 +1148,6 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
11541148
JsonResultWriteResponseOfTAsyncMethod.MakeGenericMethod(returnType),
11551149
HttpResponseExpr,
11561150
methodCall,
1157-
factoryContext.JsonSerializerOptionsExpression,
11581151
Expression.Constant(jsonTypeInfo, typeof(JsonTypeInfo<>).MakeGenericType(returnType)));
11591152
}
11601153
}
@@ -2107,39 +2100,39 @@ private static MemberInfo GetMemberInfo<T>(Expression<T> expr)
21072100
// if necessary and restart the cycle until we've reached a terminal state (unknown type).
21082101
// We currently don't handle Task<unknown> or ValueTask<unknown>. We can support this later if this
21092102
// ends up being a common scenario.
2110-
private static Task ExecuteValueTaskOfObject(ValueTask<object> valueTask, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<object> jsonTypeInfo)
2103+
private static Task ExecuteValueTaskOfObject(ValueTask<object> valueTask, HttpContext httpContext, JsonTypeInfo<object> jsonTypeInfo)
21112104
{
2112-
static async Task ExecuteAwaited(ValueTask<object> valueTask, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<object> jsonTypeInfo)
2105+
static async Task ExecuteAwaited(ValueTask<object> valueTask, HttpContext httpContext, JsonTypeInfo<object> jsonTypeInfo)
21132106
{
2114-
await ExecuteAwaitedReturn(await valueTask, httpContext, options, jsonTypeInfo);
2107+
await ExecuteAwaitedReturn(await valueTask, httpContext, jsonTypeInfo);
21152108
}
21162109

21172110
if (valueTask.IsCompletedSuccessfully)
21182111
{
2119-
return ExecuteAwaitedReturn(valueTask.GetAwaiter().GetResult(), httpContext, options, jsonTypeInfo);
2112+
return ExecuteAwaitedReturn(valueTask.GetAwaiter().GetResult(), httpContext, jsonTypeInfo);
21202113
}
21212114

2122-
return ExecuteAwaited(valueTask, httpContext, options, jsonTypeInfo);
2115+
return ExecuteAwaited(valueTask, httpContext, jsonTypeInfo);
21232116
}
21242117

2125-
private static Task ExecuteTaskOfObject(Task<object> task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<object> jsonTypeInfo)
2118+
private static Task ExecuteTaskOfObject(Task<object> task, HttpContext httpContext, JsonTypeInfo<object> jsonTypeInfo)
21262119
{
2127-
static async Task ExecuteAwaited(Task<object> task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<object> jsonTypeInfo)
2120+
static async Task ExecuteAwaited(Task<object> task, HttpContext httpContext, JsonTypeInfo<object> jsonTypeInfo)
21282121
{
2129-
await ExecuteAwaitedReturn(await task, httpContext, options, jsonTypeInfo);
2122+
await ExecuteAwaitedReturn(await task, httpContext, jsonTypeInfo);
21302123
}
21312124

21322125
if (task.IsCompletedSuccessfully)
21332126
{
2134-
return ExecuteAwaitedReturn(task.GetAwaiter().GetResult(), httpContext, options, jsonTypeInfo);
2127+
return ExecuteAwaitedReturn(task.GetAwaiter().GetResult(), httpContext, jsonTypeInfo);
21352128
}
21362129

2137-
return ExecuteAwaited(task, httpContext, options, jsonTypeInfo);
2130+
return ExecuteAwaited(task, httpContext, jsonTypeInfo);
21382131
}
21392132

2140-
private static Task ExecuteAwaitedReturn(object obj, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<object> jsonTypeInfo)
2133+
private static Task ExecuteAwaitedReturn(object obj, HttpContext httpContext, JsonTypeInfo<object> jsonTypeInfo)
21412134
{
2142-
return ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, options, jsonTypeInfo);
2135+
return ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, jsonTypeInfo);
21432136
}
21442137

21452138
private static Task ExecuteTaskOfTFast<T>(Task<T> task, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo)
@@ -2159,21 +2152,21 @@ static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext, JsonType
21592152
return ExecuteAwaited(task, httpContext, jsonTypeInfo);
21602153
}
21612154

2162-
private static Task ExecuteTaskOfT<T>(Task<T> task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<T> jsonTypeInfo)
2155+
private static Task ExecuteTaskOfT<T>(Task<T> task, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo)
21632156
{
21642157
EnsureRequestTaskNotNull(task);
21652158

2166-
static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<T> jsonTypeInfo)
2159+
static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo)
21672160
{
2168-
await WriteJsonResponse(httpContext.Response, await task, options, jsonTypeInfo);
2161+
await WriteJsonResponse(httpContext.Response, await task, jsonTypeInfo);
21692162
}
21702163

21712164
if (task.IsCompletedSuccessfully)
21722165
{
2173-
return WriteJsonResponse(httpContext.Response, task.GetAwaiter().GetResult(), options, jsonTypeInfo);
2166+
return WriteJsonResponse(httpContext.Response, task.GetAwaiter().GetResult(), jsonTypeInfo);
21742167
}
21752168

2176-
return ExecuteAwaited(task, httpContext, options, jsonTypeInfo);
2169+
return ExecuteAwaited(task, httpContext, jsonTypeInfo);
21772170
}
21782171

21792172
private static Task ExecuteTaskOfString(Task<string?> task, HttpContext httpContext)
@@ -2264,19 +2257,19 @@ static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext, Jso
22642257
return ExecuteAwaited(task, httpContext, jsonTypeInfo);
22652258
}
22662259

2267-
private static Task ExecuteValueTaskOfT<T>(ValueTask<T> task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<T> jsonTypeInfo)
2260+
private static Task ExecuteValueTaskOfT<T>(ValueTask<T> task, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo)
22682261
{
2269-
static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<T> jsonTypeInfo)
2262+
static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo)
22702263
{
2271-
await WriteJsonResponse(httpContext.Response, await task, options, jsonTypeInfo);
2264+
await WriteJsonResponse(httpContext.Response, await task, jsonTypeInfo);
22722265
}
22732266

22742267
if (task.IsCompletedSuccessfully)
22752268
{
2276-
return WriteJsonResponse(httpContext.Response, task.GetAwaiter().GetResult(), options, jsonTypeInfo);
2269+
return WriteJsonResponse(httpContext.Response, task.GetAwaiter().GetResult(), jsonTypeInfo);
22772270
}
22782271

2279-
return ExecuteAwaited(task, httpContext, options, jsonTypeInfo);
2272+
return ExecuteAwaited(task, httpContext, jsonTypeInfo);
22802273
}
22812274

22822275
private static Task ExecuteValueTaskOfString(ValueTask<string?> task, HttpContext httpContext)
@@ -2328,9 +2321,9 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex
23282321
private static Task WriteJsonResponseFast<T>(HttpResponse response, T value, JsonTypeInfo<T> jsonTypeInfo)
23292322
=> HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default);
23302323

2331-
private static Task WriteJsonResponse<T>(HttpResponse response, T? value, JsonSerializerOptions options, JsonTypeInfo<T> jsonTypeInfo)
2324+
private static Task WriteJsonResponse<T>(HttpResponse response, T? value, JsonTypeInfo<T> jsonTypeInfo)
23322325
{
2333-
return ExecuteHandlerHelper.WriteJsonResponseAsync(response, value, options, jsonTypeInfo);
2326+
return ExecuteHandlerHelper.WriteJsonResponseAsync(response, value, jsonTypeInfo);
23342327
}
23352328

23362329
private static NotSupportedException GetUnsupportedReturnTypeException(Type returnType)

src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,4 @@ internal sealed class RequestDelegateFactoryContext
5959

6060
// Grab these options upfront to avoid the per request DI scope that would be made otherwise to get the options when writing Json
6161
public required JsonSerializerOptions JsonSerializerOptions { get; set; }
62-
public required Expression JsonSerializerOptionsExpression { get; set; }
6362
}

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

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,46 +2033,11 @@ public async Task RequestDelegateWritesJsonTypeDiscriminatorToJsonResponseBody_W
20332033
Assert.Equal(nameof(JsonTodoChild), deserializedResponseBody["$type"]!.GetValue<string>());
20342034
}
20352035

2036-
public static IEnumerable<object[]> JsonContextActions
2037-
{
2038-
get
2039-
{
2040-
return ComplexResult.Concat(ChildResult);
2041-
}
2042-
}
2043-
20442036
[JsonSerializable(typeof(Todo))]
20452037
[JsonSerializable(typeof(TodoChild))]
20462038
private partial class TestJsonContext : JsonSerializerContext
20472039
{ }
20482040

2049-
[Theory]
2050-
[MemberData(nameof(JsonContextActions))]
2051-
public async Task RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerContext(Delegate @delegate)
2052-
{
2053-
var httpContext = CreateHttpContext();
2054-
httpContext.RequestServices = new ServiceCollection()
2055-
.AddSingleton(LoggerFactory)
2056-
.ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default)
2057-
.BuildServiceProvider();
2058-
2059-
var responseBodyStream = new MemoryStream();
2060-
httpContext.Response.Body = responseBodyStream;
2061-
2062-
var factoryResult = RequestDelegateFactory.Create(@delegate);
2063-
var requestDelegate = factoryResult.RequestDelegate;
2064-
2065-
await requestDelegate(httpContext);
2066-
2067-
var deserializedResponseBody = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), new JsonSerializerOptions
2068-
{
2069-
PropertyNameCaseInsensitive = true
2070-
});
2071-
2072-
Assert.NotNull(deserializedResponseBody);
2073-
Assert.Equal("Write even more tests!", deserializedResponseBody!.Name);
2074-
}
2075-
20762041
[Fact]
20772042
public void CreateDelegateThrows_WhenGetJsonTypeInfoFail()
20782043
{

src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,6 @@ public ServiceProvider CreateServiceProvider(Action<IServiceCollection> configur
192192
{
193193
var serviceCollection = new ServiceCollection();
194194
serviceCollection.AddSingleton(LoggerFactory);
195-
var jsonOptions = new JsonOptions();
196-
serviceCollection.AddSingleton(Options.Create(jsonOptions));
197195
if (configureServices is not null)
198196
{
199197
configureServices(serviceCollection);

0 commit comments

Comments
 (0)