Skip to content

Commit 0d52a44

Browse files
authored
Adding STJ Polymorphism to Result Types (dotnet#46008)
* Adding STJ Polymorphism to Result Types * Renaming unittest * Adding unit tests * Adding more unit tests * Setting DefaultTypeInfoResolver * Removing ISTrimmable * Removing cache * Clean up * Avoiding multiple GetTypeInfo calls * Fixing JsonResult * Clean up * clean up * Adding Json apis proposal * Removing name change Removing the change from HttpResultsHelper to HttpResultsWriter to avoid polluting the git with not related changes and I will do it later. * Fixing bad merge * Fix build * PR review * PR Feedback * Update for the approved API * PR review * Update TypedResultsTests.cs * Changing IsPolymorphicSafe * Fixing notnull annotation
1 parent ea2c82c commit 0d52a44

15 files changed

+519
-92
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
10551055
{
10561056
var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg);
10571057

1058-
if (jsonTypeInfo.IsPolymorphicSafe() == true)
1058+
if (jsonTypeInfo.HasKnownPolymorphism())
10591059
{
10601060
return Expression.Call(
10611061
ExecuteTaskOfTFastMethod.MakeGenericMethod(typeArg),
@@ -1096,7 +1096,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
10961096
{
10971097
var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg);
10981098

1099-
if (jsonTypeInfo.IsPolymorphicSafe() == true)
1099+
if (jsonTypeInfo.HasKnownPolymorphism())
11001100
{
11011101
return Expression.Call(
11021102
ExecuteValueTaskOfTFastMethod.MakeGenericMethod(typeArg),
@@ -1140,7 +1140,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
11401140
{
11411141
var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(returnType);
11421142

1143-
if (jsonTypeInfo.IsPolymorphicSafe() == true)
1143+
if (jsonTypeInfo.HasKnownPolymorphism())
11441144
{
11451145
return Expression.Call(
11461146
JsonResultWriteResponseOfTFastAsyncMethod.MakeGenericMethod(returnType),

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

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics.CodeAnalysis;
54
using System.Text;
65
using System.Text.Json;
6+
using System.Text.Json.Serialization.Metadata;
7+
using Microsoft.AspNetCore.Http.Json;
78
using Microsoft.AspNetCore.Internal;
89
using Microsoft.AspNetCore.Mvc;
10+
using Microsoft.Extensions.DependencyInjection;
911
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Options;
1013
using Microsoft.Net.Http.Headers;
1114

1215
namespace Microsoft.AspNetCore.Http;
@@ -16,13 +19,10 @@ internal static partial class HttpResultsHelper
1619
internal const string DefaultContentType = "text/plain; charset=utf-8";
1720
private static readonly Encoding DefaultEncoding = Encoding.UTF8;
1821

19-
// Remove once https://github.com/dotnet/aspnetcore/pull/46008 is done.
20-
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
21-
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
22-
public static Task WriteResultAsJsonAsync<T>(
22+
public static Task WriteResultAsJsonAsync<TValue>(
2323
HttpContext httpContext,
2424
ILogger logger,
25-
T? value,
25+
TValue? value,
2626
string? contentType = null,
2727
JsonSerializerOptions? jsonSerializerOptions = null)
2828
{
@@ -31,32 +31,30 @@ public static Task WriteResultAsJsonAsync<T>(
3131
return Task.CompletedTask;
3232
}
3333

34-
var declaredType = typeof(T);
35-
if (declaredType.IsValueType)
36-
{
37-
Log.WritingResultAsJson(logger, declaredType.Name);
34+
jsonSerializerOptions ??= ResolveJsonOptions(httpContext).SerializerOptions;
35+
var jsonTypeInfo = (JsonTypeInfo<TValue>)jsonSerializerOptions.GetTypeInfo(typeof(TValue));
3836

39-
// In this case the polymorphism is not
40-
// relevant and we don't need to box.
37+
Type? runtimeType;
38+
if (jsonTypeInfo.IsValid(runtimeType = value.GetType()))
39+
{
40+
Log.WritingResultAsJson(logger, jsonTypeInfo.Type.Name);
4141
return httpContext.Response.WriteAsJsonAsync(
42-
value,
43-
options: jsonSerializerOptions,
44-
contentType: contentType);
42+
value,
43+
jsonTypeInfo,
44+
contentType: contentType);
4545
}
4646

47-
var runtimeType = value.GetType();
48-
4947
Log.WritingResultAsJson(logger, runtimeType.Name);
50-
51-
// Call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type
48+
// Since we don't know the type's polymorphic characteristics
49+
// our best option is use the runtime type, so,
50+
// call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type
5251
// and avoid source generators issues.
5352
// https://github.com/dotnet/aspnetcore/issues/43894
5453
// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
5554
return httpContext.Response.WriteAsJsonAsync(
56-
value,
57-
runtimeType,
58-
options: jsonSerializerOptions,
59-
contentType: contentType);
55+
value,
56+
jsonSerializerOptions.GetTypeInfo(runtimeType),
57+
contentType: contentType);
6058
}
6159

6260
public static Task WriteResultAsContentAsync(
@@ -146,6 +144,12 @@ public static void ApplyProblemDetailsDefaultsIfNeeded(object? value, int? statu
146144
}
147145
}
148146

147+
private static JsonOptions ResolveJsonOptions(HttpContext httpContext)
148+
{
149+
// Attempt to resolve options from DI then fallback to default options
150+
return httpContext.RequestServices.GetService<IOptions<JsonOptions>>()?.Value ?? new JsonOptions();
151+
}
152+
149153
internal static partial class Log
150154
{
151155
[LoggerMessage(1, LogLevel.Information,

src/Http/Http.Results/src/JsonHttpResultOfT.cs

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Text.Json;
6+
using System.Text.Json.Serialization.Metadata;
57
using Microsoft.AspNetCore.Mvc;
68
using Microsoft.Extensions.DependencyInjection;
79
using Microsoft.Extensions.Logging;
@@ -18,33 +20,33 @@ public sealed partial class JsonHttpResult<TValue> : IResult, IStatusCodeHttpRes
1820
/// </summary>
1921
/// <param name="value">The value to format in the entity body.</param>
2022
/// <param name="jsonSerializerOptions">The serializer settings.</param>
21-
internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOptions)
22-
: this(value, statusCode: null, contentType: null, jsonSerializerOptions: jsonSerializerOptions)
23-
{
24-
}
25-
26-
/// <summary>
27-
/// Initializes a new instance of the <see cref="Json"/> class with the values.
28-
/// </summary>
29-
/// <param name="value">The value to format in the entity body.</param>
3023
/// <param name="statusCode">The HTTP status code of the response.</param>
31-
/// <param name="jsonSerializerOptions">The serializer settings.</param>
32-
internal JsonHttpResult(TValue? value, int? statusCode, JsonSerializerOptions? jsonSerializerOptions)
33-
: this(value, statusCode: statusCode, contentType: null, jsonSerializerOptions: jsonSerializerOptions)
24+
/// <param name="contentType">The value for the <c>Content-Type</c> header</param>
25+
[RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)]
26+
[RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)]
27+
internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOptions, int? statusCode = null, string? contentType = null)
3428
{
29+
Value = value;
30+
ContentType = contentType;
31+
32+
if (value is ProblemDetails problemDetails)
33+
{
34+
ProblemDetailsDefaults.Apply(problemDetails, statusCode);
35+
statusCode ??= problemDetails.Status;
36+
}
37+
StatusCode = statusCode;
38+
39+
if (jsonSerializerOptions is not null && !jsonSerializerOptions.IsReadOnly)
40+
{
41+
jsonSerializerOptions.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver();
42+
}
43+
44+
JsonSerializerOptions = jsonSerializerOptions;
3545
}
3646

37-
/// <summary>
38-
/// Initializes a new instance of the <see cref="Json"/> class with the values.
39-
/// </summary>
40-
/// <param name="value">The value to format in the entity body.</param>
41-
/// <param name="statusCode">The HTTP status code of the response.</param>
42-
/// <param name="jsonSerializerOptions">The serializer settings.</param>
43-
/// <param name="contentType">The value for the <c>Content-Type</c> header</param>
44-
internal JsonHttpResult(TValue? value, int? statusCode, string? contentType, JsonSerializerOptions? jsonSerializerOptions)
47+
internal JsonHttpResult(TValue? value, int? statusCode = null, string? contentType = null)
4548
{
4649
Value = value;
47-
JsonSerializerOptions = jsonSerializerOptions;
4850
ContentType = contentType;
4951

5052
if (value is ProblemDetails problemDetails)
@@ -59,7 +61,12 @@ internal JsonHttpResult(TValue? value, int? statusCode, string? contentType, Jso
5961
/// <summary>
6062
/// Gets or sets the serializer settings.
6163
/// </summary>
62-
public JsonSerializerOptions? JsonSerializerOptions { get; internal init; }
64+
public JsonSerializerOptions? JsonSerializerOptions { get; }
65+
66+
/// <summary>
67+
/// Gets or sets the serializer settings.
68+
/// </summary>
69+
internal JsonTypeInfo? JsonTypeInfo { get; init; }
6370

6471
/// <summary>
6572
/// Gets the object result.
@@ -71,7 +78,7 @@ internal JsonHttpResult(TValue? value, int? statusCode, string? contentType, Jso
7178
/// <summary>
7279
/// Gets the value for the <c>Content-Type</c> header.
7380
/// </summary>
74-
public string? ContentType { get; internal set; }
81+
public string? ContentType { get; }
7582

7683
/// <summary>
7784
/// Gets the HTTP status code.
@@ -93,6 +100,30 @@ public Task ExecuteAsync(HttpContext httpContext)
93100
httpContext.Response.StatusCode = statusCode;
94101
}
95102

103+
if (Value is null)
104+
{
105+
return Task.CompletedTask;
106+
}
107+
108+
if (JsonTypeInfo != null)
109+
{
110+
HttpResultsHelper.Log.WritingResultAsJson(logger, JsonTypeInfo.Type.Name);
111+
112+
if (JsonTypeInfo is JsonTypeInfo<TValue> typedJsonTypeInfo)
113+
{
114+
// We don't need to box here.
115+
return httpContext.Response.WriteAsJsonAsync(
116+
Value,
117+
typedJsonTypeInfo,
118+
contentType: ContentType);
119+
}
120+
121+
return httpContext.Response.WriteAsJsonAsync(
122+
Value,
123+
JsonTypeInfo,
124+
contentType: ContentType);
125+
}
126+
96127
return HttpResultsHelper.WriteResultAsJsonAsync(
97128
httpContext,
98129
logger,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
namespace Microsoft.AspNetCore.Http;
5+
6+
internal class JsonHttpResultTrimmerWarning
7+
{
8+
public const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext.";
9+
public const string SerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the overload that takes a JsonTypeInfo or JsonSerializerContext.";
10+
11+
}

src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<Compile Include="$(SharedSourceRoot)ProblemDetails\ProblemDetailsDefaults.cs" LinkBase="Shared" />
2020
<Compile Include="$(SharedSourceRoot)ApiExplorerTypes\*.cs" LinkBase="Shared" />
2121
<Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" />
22+
<Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared"/>
2223
<Compile Include="$(SharedSourceRoot)RouteValueDictionaryTrimmerWarning.cs" LinkBase="Shared" />
2324
</ItemGroup>
2425

src/Http/Http.Results/src/PublicAPI.Unshipped.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ static Microsoft.AspNetCore.Http.Results.Created(string? uri, object? value) ->
1212
static Microsoft.AspNetCore.Http.Results.Created(System.Uri? uri, object? value) -> Microsoft.AspNetCore.Http.IResult!
1313
static Microsoft.AspNetCore.Http.Results.Created<TValue>(string? uri, TValue? value) -> Microsoft.AspNetCore.Http.IResult!
1414
static Microsoft.AspNetCore.Http.Results.Created<TValue>(System.Uri? uri, TValue? value) -> Microsoft.AspNetCore.Http.IResult!
15+
static Microsoft.AspNetCore.Http.Results.Json(object? data, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
16+
static Microsoft.AspNetCore.Http.Results.Json(object? data, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
17+
static Microsoft.AspNetCore.Http.Results.Json<TValue>(TValue? data, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
18+
static Microsoft.AspNetCore.Http.Results.Json<TValue>(TValue? data, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue>! jsonTypeInfo, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
1519
static Microsoft.AspNetCore.Http.TypedResults.Created() -> Microsoft.AspNetCore.Http.HttpResults.Created!
1620
static Microsoft.AspNetCore.Http.TypedResults.Created(string? uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
1721
static Microsoft.AspNetCore.Http.TypedResults.Created(System.Uri? uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
@@ -22,4 +26,6 @@ static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(System.Uri? uri, T
2226
*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created(System.Uri! uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
2327
*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created(string! uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
2428
*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(System.Uri! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created<TValue>!
25-
*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(string! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created<TValue>!
29+
*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(string! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created<TValue>!
30+
static Microsoft.AspNetCore.Http.TypedResults.Json<TValue>(TValue? data, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<TValue>!
31+
static Microsoft.AspNetCore.Http.TypedResults.Json<TValue>(TValue? data, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue>! jsonTypeInfo, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<TValue>!

0 commit comments

Comments
 (0)