Skip to content

Commit d27c95d

Browse files
JamesNKeerhardt
andauthored
[AOT] Add expression free request filter pipeline for RequestDelegate (#46020)
Co-authored-by: Eric Erhardt <[email protected]>
1 parent 334da01 commit d27c95d

11 files changed

+400
-106
lines changed

src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="$(SharedSourceRoot)ProblemDetails\ProblemDetailsDefaults.cs" LinkBase="Shared" />
2424
<Compile Include="$(SharedSourceRoot)ValueStringBuilder\**\*.cs" LinkBase="Shared"/>
2525
<Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared"/>
26+
<Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared"/>
2627
</ItemGroup>
2728

2829
<ItemGroup>

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

+10-42
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.AspNetCore.Http.Features;
1818
using Microsoft.AspNetCore.Http.Json;
1919
using Microsoft.AspNetCore.Http.Metadata;
20+
using Microsoft.AspNetCore.Internal;
2021
using Microsoft.AspNetCore.Routing;
2122
using Microsoft.Extensions.DependencyInjection;
2223
using Microsoft.Extensions.Internal;
@@ -260,12 +261,12 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func<HttpConte
260261

261262
private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options, RequestDelegateMetadataResult? metadataResult = null, Delegate? handler = null)
262263
{
263-
if (metadataResult?.CachedFactoryContext is not null)
264+
if (metadataResult?.CachedFactoryContext is RequestDelegateFactoryContext cachedFactoryContext)
264265
{
265-
metadataResult.CachedFactoryContext.MetadataAlreadyInferred = true;
266+
cachedFactoryContext.MetadataAlreadyInferred = true;
266267
// The handler was not passed in to the InferMetadata call that originally created this context.
267-
metadataResult.CachedFactoryContext.Handler = handler;
268-
return metadataResult.CachedFactoryContext;
268+
cachedFactoryContext.Handler = handler;
269+
return cachedFactoryContext;
269270
}
270271

271272
var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance;
@@ -2135,21 +2136,7 @@ static async Task ExecuteAwaited(Task<object> task, HttpContext httpContext, Jso
21352136

21362137
private static Task ExecuteAwaitedReturn(object obj, HttpContext httpContext, JsonSerializerOptions options, JsonTypeInfo<object> jsonTypeInfo)
21372138
{
2138-
// Terminal built ins
2139-
if (obj is IResult result)
2140-
{
2141-
return ExecuteResultWriteResponse(result, httpContext);
2142-
}
2143-
else if (obj is string stringValue)
2144-
{
2145-
SetPlaintextContentType(httpContext);
2146-
return httpContext.Response.WriteAsync(stringValue);
2147-
}
2148-
else
2149-
{
2150-
// Otherwise, we JSON serialize when we reach the terminal state
2151-
return WriteJsonResponse(httpContext.Response, obj, options, jsonTypeInfo);
2152-
}
2139+
return ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, options, jsonTypeInfo);
21532140
}
21542141

21552142
private static Task ExecuteTaskOfTFast<T>(Task<T> task, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo)
@@ -2188,7 +2175,7 @@ static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext, JsonSeri
21882175

21892176
private static Task ExecuteTaskOfString(Task<string?> task, HttpContext httpContext)
21902177
{
2191-
SetPlaintextContentType(httpContext);
2178+
ExecuteHandlerHelper.SetPlaintextContentType(httpContext);
21922179
EnsureRequestTaskNotNull(task);
21932180

21942181
static async Task ExecuteAwaited(Task<string> task, HttpContext httpContext)
@@ -2206,7 +2193,7 @@ static async Task ExecuteAwaited(Task<string> task, HttpContext httpContext)
22062193

22072194
private static Task ExecuteWriteStringResponseAsync(HttpContext httpContext, string text)
22082195
{
2209-
SetPlaintextContentType(httpContext);
2196+
ExecuteHandlerHelper.SetPlaintextContentType(httpContext);
22102197
return httpContext.Response.WriteAsync(text);
22112198
}
22122199

@@ -2293,7 +2280,7 @@ static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext, Jso
22932280

22942281
private static Task ExecuteValueTaskOfString(ValueTask<string?> task, HttpContext httpContext)
22952282
{
2296-
SetPlaintextContentType(httpContext);
2283+
ExecuteHandlerHelper.SetPlaintextContentType(httpContext);
22972284

22982285
static async Task ExecuteAwaited(ValueTask<string> task, HttpContext httpContext)
22992286
{
@@ -2342,21 +2329,7 @@ private static Task WriteJsonResponseFast<T>(HttpResponse response, T value, Jso
23422329

23432330
private static Task WriteJsonResponse<T>(HttpResponse response, T? value, JsonSerializerOptions options, JsonTypeInfo<T> jsonTypeInfo)
23442331
{
2345-
var runtimeType = value?.GetType();
2346-
2347-
if (runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.IsPolymorphicSafe())
2348-
{
2349-
// In this case the polymorphism is not
2350-
// relevant for us and will be handled by STJ, if needed.
2351-
return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, jsonTypeInfo, default);
2352-
}
2353-
2354-
// Call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type
2355-
// and avoid source generators issues.
2356-
// https://github.com/dotnet/aspnetcore/issues/43894
2357-
// https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
2358-
var runtimeTypeInfo = options.GetTypeInfo(runtimeType);
2359-
return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, runtimeTypeInfo, default);
2332+
return ExecuteHandlerHelper.WriteJsonResponseAsync(response, value, options, jsonTypeInfo);
23602333
}
23612334

23622335
private static NotSupportedException GetUnsupportedReturnTypeException(Type returnType)
@@ -2545,11 +2518,6 @@ private static IResult EnsureRequestResultNotNull(IResult? result)
25452518
return result;
25462519
}
25472520

2548-
private static void SetPlaintextContentType(HttpContext httpContext)
2549-
{
2550-
httpContext.Response.ContentType ??= "text/plain; charset=utf-8";
2551-
}
2552-
25532521
private static string BuildErrorMessageForMultipleBodyParameters(RequestDelegateFactoryContext factoryContext)
25542522
{
25552523
var errorMessage = new StringBuilder();

src/Http/Http.Extensions/src/RequestDelegateMetadataResult.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ public sealed class RequestDelegateMetadataResult
1919

2020
// This internal cached context avoids redoing unnecessary reflection in Create that was already done in InferMetadata.
2121
// InferMetadata currently does more work than it needs to building up expression trees, but the expectation is that InferMetadata will usually be followed by Create.
22-
internal RequestDelegateFactoryContext? CachedFactoryContext { get; set; }
22+
// The property is typed as object to avoid having a dependency System.Linq.Expressions. The value is RequestDelegateFactoryContext.
23+
internal object? CachedFactoryContext { get; set; }
2324
}

src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,25 @@ private static IEndpointConventionBuilder Map(
196196
ArgumentNullException.ThrowIfNull(pattern);
197197
ArgumentNullException.ThrowIfNull(requestDelegate);
198198

199-
return endpoints.GetOrAddRouteEndpointDataSource().AddRequestDelegate(pattern, requestDelegate, httpMethods);
199+
return endpoints
200+
.GetOrAddRouteEndpointDataSource()
201+
.AddRequestDelegate(pattern, requestDelegate, httpMethods, CreateHandlerRequestDelegate);
202+
203+
static RequestDelegateResult CreateHandlerRequestDelegate(Delegate handler, RequestDelegateFactoryOptions options, RequestDelegateMetadataResult? metadataResult)
204+
{
205+
var requestDelegate = (RequestDelegate)handler;
206+
207+
// Create request delegate that calls filter pipeline.
208+
if (options.EndpointBuilder?.FilterFactories.Count > 0)
209+
{
210+
requestDelegate = RequestDelegateFilterPipelineBuilder.Create(requestDelegate, options);
211+
}
212+
213+
IReadOnlyList<object> metadata = options.EndpointBuilder?.Metadata is not null ?
214+
new List<object>(options.EndpointBuilder.Metadata) :
215+
Array.Empty<object>();
216+
return new RequestDelegateResult(requestDelegate, metadata);
217+
}
200218
}
201219

202220
/// <summary>
@@ -416,7 +434,9 @@ private static RouteHandlerBuilder Map(
416434
ArgumentNullException.ThrowIfNull(pattern);
417435
ArgumentNullException.ThrowIfNull(handler);
418436

419-
return endpoints.GetOrAddRouteEndpointDataSource().AddRouteHandler(pattern, handler, httpMethods, isFallback);
437+
return endpoints
438+
.GetOrAddRouteEndpointDataSource()
439+
.AddRouteHandler(pattern, handler, httpMethods, isFallback, RequestDelegateFactory.InferMetadata, RequestDelegateFactory.Create);
420440
}
421441

422442
private static RouteEndpointDataSource GetOrAddRouteEndpointDataSource(this IEndpointRouteBuilder endpoints)

src/Http/Routing/src/Builder/RouteHandlerBuilder.cs

-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
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 Microsoft.AspNetCore.Routing;
5-
64
namespace Microsoft.AspNetCore.Builder;
75

86
/// <summary>
@@ -14,12 +12,6 @@ public sealed class RouteHandlerBuilder : IEndpointConventionBuilder
1412
private readonly ICollection<Action<EndpointBuilder>>? _conventions;
1513
private readonly ICollection<Action<EndpointBuilder>>? _finallyConventions;
1614

17-
/// <summary>
18-
/// Instantiates a new <see cref="RouteHandlerBuilder" /> given a ThrowOnAddAfterEndpointBuiltConventionCollection from
19-
/// <see cref="RouteEndpointDataSource.AddRouteHandler(Routing.Patterns.RoutePattern, Delegate, IEnumerable{string}?, bool)"/>.
20-
/// </summary>
21-
/// <param name="conventions">The convention list returned from <see cref="RouteEndpointDataSource"/>.</param>
22-
/// <param name="finallyConventions">The final convention list returned from <see cref="RouteEndpointDataSource"/>.</param>
2315
internal RouteHandlerBuilder(ICollection<Action<EndpointBuilder>> conventions, ICollection<Action<EndpointBuilder>> finallyConventions)
2416
{
2517
_conventions = conventions;

src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
<Compile Include="$(SharedSourceRoot)MediaType\HttpTokenParsingRule.cs" LinkBase="Shared" />
3232
<Compile Include="$(SharedSourceRoot)ApiExplorerTypes\*.cs" LinkBase="Shared" />
3333
<Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" />
34+
<Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared"/>
35+
<Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared"/>
3436
</ItemGroup>
3537

3638
<ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.Diagnostics;
5+
using System.Text.Json.Serialization.Metadata;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Http.Json;
8+
using Microsoft.AspNetCore.Internal;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.AspNetCore.Routing;
13+
14+
internal static class RequestDelegateFilterPipelineBuilder
15+
{
16+
// Due to https://github.com/dotnet/aspnetcore/issues/41330 we cannot reference the EmptyHttpResult type
17+
// but users still need to assert on it as in https://github.com/dotnet/aspnetcore/issues/45063
18+
// so we temporarily work around this here by using reflection to get the actual type.
19+
private static readonly object? EmptyHttpResultInstance = Type.GetType("Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult, Microsoft.AspNetCore.Http.Results")?.GetProperty("Instance")?.GetValue(null, null);
20+
21+
public static RequestDelegate Create(RequestDelegate requestDelegate, RequestDelegateFactoryOptions options)
22+
{
23+
Debug.Assert(options.EndpointBuilder != null);
24+
25+
var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices;
26+
var jsonOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value ?? new JsonOptions();
27+
var jsonSerializerOptions = jsonOptions.SerializerOptions;
28+
29+
var factoryContext = new EndpointFilterFactoryContext
30+
{
31+
MethodInfo = requestDelegate.Method,
32+
ApplicationServices = options.EndpointBuilder.ApplicationServices
33+
};
34+
var jsonTypeInfo = (JsonTypeInfo<object>)jsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object));
35+
36+
EndpointFilterDelegate filteredInvocation = async (EndpointFilterInvocationContext context) =>
37+
{
38+
Debug.Assert(EmptyHttpResultInstance != null, "Unable to get EmptyHttpResult instance via reflection.");
39+
if (context.HttpContext.Response.StatusCode < 400)
40+
{
41+
await requestDelegate(context.HttpContext);
42+
}
43+
return EmptyHttpResultInstance;
44+
};
45+
46+
var initialFilteredInvocation = filteredInvocation;
47+
for (var i = options.EndpointBuilder.FilterFactories.Count - 1; i >= 0; i--)
48+
{
49+
var currentFilterFactory = options.EndpointBuilder.FilterFactories[i];
50+
filteredInvocation = currentFilterFactory(factoryContext, filteredInvocation);
51+
}
52+
53+
// The filter factories have run without modifying per-request behavior, we can skip running the pipeline.
54+
if (ReferenceEquals(initialFilteredInvocation, filteredInvocation))
55+
{
56+
return requestDelegate;
57+
}
58+
59+
return async (HttpContext httpContext) =>
60+
{
61+
var obj = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext, new object[] { httpContext }));
62+
if (obj is not null)
63+
{
64+
await ExecuteHandlerHelper.ExecuteReturnAsync(obj, httpContext, jsonSerializerOptions, jsonTypeInfo);
65+
}
66+
};
67+
}
68+
}

0 commit comments

Comments
 (0)