Skip to content

Commit c52fd93

Browse files
authored
Add short circuit in routing (#46713)
1 parent e7603bc commit c52fd93

11 files changed

+581
-3
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 BenchmarkDotNet.Attributes;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Routing.Matching;
9+
using Microsoft.AspNetCore.Routing.ShortCircuit;
10+
using Microsoft.Extensions.Logging.Abstractions;
11+
using Microsoft.Extensions.Options;
12+
using Microsoft.Extensions.Primitives;
13+
14+
namespace Microsoft.AspNetCore.Routing;
15+
16+
public class EndpointRoutingShortCircuitBenchmark
17+
{
18+
private EndpointRoutingMiddleware _normalEndpointMiddleware;
19+
private EndpointRoutingMiddleware _shortCircuitEndpointMiddleware;
20+
21+
[GlobalSetup]
22+
public void Setup()
23+
{
24+
var normalEndpoint = new Endpoint(context => Task.CompletedTask, new EndpointMetadataCollection(), "normal");
25+
26+
_normalEndpointMiddleware = new EndpointRoutingMiddleware(
27+
new BenchmarkMatcherFactory(normalEndpoint),
28+
NullLogger<EndpointRoutingMiddleware>.Instance,
29+
new BenchmarkEndpointRouteBuilder(),
30+
new BenchmarkEndpointDataSource(),
31+
new DiagnosticListener("benchmark"),
32+
Options.Create(new RouteOptions()),
33+
context => Task.CompletedTask);
34+
35+
var shortCircuitEndpoint = new Endpoint(context => Task.CompletedTask, new EndpointMetadataCollection(new ShortCircuitMetadata(200)), "shortcircuit");
36+
37+
_shortCircuitEndpointMiddleware = new EndpointRoutingMiddleware(
38+
new BenchmarkMatcherFactory(shortCircuitEndpoint),
39+
NullLogger<EndpointRoutingMiddleware>.Instance,
40+
new BenchmarkEndpointRouteBuilder(),
41+
new BenchmarkEndpointDataSource(),
42+
new DiagnosticListener("benchmark"),
43+
Options.Create(new RouteOptions()),
44+
context => Task.CompletedTask);
45+
46+
}
47+
48+
[Benchmark]
49+
public async Task NormalEndpoint()
50+
{
51+
var context = new DefaultHttpContext();
52+
await _normalEndpointMiddleware.Invoke(context);
53+
}
54+
55+
[Benchmark]
56+
public async Task ShortCircuitEndpoint()
57+
{
58+
var context = new DefaultHttpContext();
59+
await _shortCircuitEndpointMiddleware.Invoke(context);
60+
}
61+
}
62+
63+
internal class BenchmarkMatcherFactory : MatcherFactory
64+
{
65+
private readonly Endpoint _endpoint;
66+
67+
public BenchmarkMatcherFactory(Endpoint endpoint)
68+
{
69+
_endpoint = endpoint;
70+
}
71+
72+
public override Matcher CreateMatcher(EndpointDataSource dataSource)
73+
{
74+
return new BenchmarkMatcher(_endpoint);
75+
}
76+
77+
internal class BenchmarkMatcher : Matcher
78+
{
79+
private Endpoint _endpoint;
80+
81+
public BenchmarkMatcher(Endpoint endpoint)
82+
{
83+
_endpoint = endpoint;
84+
}
85+
86+
public override Task MatchAsync(HttpContext httpContext)
87+
{
88+
httpContext.SetEndpoint(_endpoint);
89+
return Task.CompletedTask;
90+
}
91+
}
92+
}
93+
94+
internal class BenchmarkEndpointRouteBuilder : IEndpointRouteBuilder
95+
{
96+
public IServiceProvider ServiceProvider => throw new NotImplementedException();
97+
98+
public ICollection<EndpointDataSource> DataSources => new List<EndpointDataSource>();
99+
100+
public IApplicationBuilder CreateApplicationBuilder()
101+
{
102+
throw new NotImplementedException();
103+
}
104+
}
105+
internal class BenchmarkEndpointDataSource : EndpointDataSource
106+
{
107+
public override IReadOnlyList<Endpoint> Endpoints => throw new NotImplementedException();
108+
109+
public override IChangeToken GetChangeToken()
110+
{
111+
throw new NotImplementedException();
112+
}
113+
}

src/Http/Routing/src/EndpointMiddleware.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public Task Invoke(HttpContext httpContext)
3333
var endpoint = httpContext.GetEndpoint();
3434
if (endpoint is not null)
3535
{
36+
// This check should be kept in sync with the one in EndpointRoutingMiddleware
3637
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
3738
{
3839
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null &&

src/Http/Routing/src/EndpointRoutingMiddleware.cs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Runtime.CompilerServices;
7+
using Microsoft.AspNetCore.Authorization;
8+
using Microsoft.AspNetCore.Cors.Infrastructure;
79
using Microsoft.AspNetCore.Http;
810
using Microsoft.AspNetCore.Routing.Matching;
11+
using Microsoft.AspNetCore.Routing.ShortCircuit;
912
using Microsoft.Extensions.Logging;
13+
using Microsoft.Extensions.Options;
1014

1115
namespace Microsoft.AspNetCore.Routing;
1216

@@ -19,7 +23,7 @@ internal sealed partial class EndpointRoutingMiddleware
1923
private readonly EndpointDataSource _endpointDataSource;
2024
private readonly DiagnosticListener _diagnosticListener;
2125
private readonly RequestDelegate _next;
22-
26+
private readonly RouteOptions _routeOptions;
2327
private Task<Matcher>? _initializationTask;
2428

2529
public EndpointRoutingMiddleware(
@@ -28,6 +32,7 @@ public EndpointRoutingMiddleware(
2832
IEndpointRouteBuilder endpointRouteBuilder,
2933
EndpointDataSource rootCompositeEndpointDataSource,
3034
DiagnosticListener diagnosticListener,
35+
IOptions<RouteOptions> routeOptions,
3136
RequestDelegate next)
3237
{
3338
ArgumentNullException.ThrowIfNull(endpointRouteBuilder);
@@ -36,6 +41,7 @@ public EndpointRoutingMiddleware(
3641
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3742
_diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
3843
_next = next ?? throw new ArgumentNullException(nameof(next));
44+
_routeOptions = routeOptions.Value;
3945

4046
// rootCompositeEndpointDataSource is a constructor parameter only so it always gets disposed by DI. This ensures that any
4147
// disposable EndpointDataSources also get disposed. _endpointDataSource is a component of rootCompositeEndpointDataSource.
@@ -102,6 +108,12 @@ private Task SetRoutingAndContinue(HttpContext httpContext)
102108
}
103109

104110
Log.MatchSuccess(_logger, endpoint);
111+
112+
var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
113+
if (shortCircuitMetadata is not null)
114+
{
115+
return ExecuteShortCircuit(shortCircuitMetadata, endpoint, httpContext);
116+
}
105117
}
106118

107119
return _next(httpContext);
@@ -115,6 +127,75 @@ static void Write(DiagnosticListener diagnosticListener, HttpContext httpContext
115127
}
116128
}
117129

130+
private Task ExecuteShortCircuit(ShortCircuitMetadata shortCircuitMetadata, Endpoint endpoint, HttpContext httpContext)
131+
{
132+
// This check should be kept in sync with the one in EndpointMiddleware
133+
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
134+
{
135+
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null)
136+
{
137+
ThrowCannotShortCircuitAnAuthRouteException(endpoint);
138+
}
139+
140+
if (endpoint.Metadata.GetMetadata<ICorsMetadata>() is not null)
141+
{
142+
ThrowCannotShortCircuitACorsRouteException(endpoint);
143+
}
144+
}
145+
146+
if (shortCircuitMetadata.StatusCode.HasValue)
147+
{
148+
httpContext.Response.StatusCode = shortCircuitMetadata.StatusCode.Value;
149+
}
150+
151+
if (endpoint.RequestDelegate is not null)
152+
{
153+
if (!_logger.IsEnabled(LogLevel.Information))
154+
{
155+
// Avoid the AwaitRequestTask state machine allocation if logging is disabled.
156+
return endpoint.RequestDelegate(httpContext);
157+
}
158+
159+
Log.ExecutingEndpoint(_logger, endpoint);
160+
161+
try
162+
{
163+
var requestTask = endpoint.RequestDelegate(httpContext);
164+
if (!requestTask.IsCompletedSuccessfully)
165+
{
166+
return AwaitRequestTask(endpoint, requestTask, _logger);
167+
}
168+
}
169+
catch
170+
{
171+
Log.ExecutedEndpoint(_logger, endpoint);
172+
throw;
173+
}
174+
175+
Log.ExecutedEndpoint(_logger, endpoint);
176+
177+
return Task.CompletedTask;
178+
179+
static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
180+
{
181+
try
182+
{
183+
await requestTask;
184+
}
185+
finally
186+
{
187+
Log.ExecutedEndpoint(logger, endpoint);
188+
}
189+
}
190+
191+
}
192+
else
193+
{
194+
Log.ShortCircuitedEndpoint(_logger, endpoint);
195+
}
196+
return Task.CompletedTask;
197+
}
198+
118199
// Initialization is async to avoid blocking threads while reflection and things
119200
// of that nature take place.
120201
//
@@ -165,6 +246,18 @@ private Task<Matcher> InitializeCoreAsync()
165246
}
166247
}
167248

249+
private static void ThrowCannotShortCircuitAnAuthRouteException(Endpoint endpoint)
250+
{
251+
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
252+
"but this endpoint is marked with short circuit and it will execute on Routing Middleware.");
253+
}
254+
255+
private static void ThrowCannotShortCircuitACorsRouteException(Endpoint endpoint)
256+
{
257+
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " +
258+
"but this endpoint is marked with short circuit and it will execute on Routing Middleware.");
259+
}
260+
168261
private static partial class Log
169262
{
170263
public static void MatchSuccess(ILogger logger, Endpoint endpoint)
@@ -181,5 +274,14 @@ public static void MatchSkipped(ILogger logger, Endpoint endpoint)
181274

182275
[LoggerMessage(3, LogLevel.Debug, "Endpoint '{EndpointName}' already set, skipping route matching.", EventName = "MatchingSkipped")]
183276
private static partial void MatchingSkipped(ILogger logger, string? endpointName);
277+
278+
[LoggerMessage(4, LogLevel.Information, "The endpoint '{EndpointName}' is being executed without running additional middleware.", EventName = "ExecutingEndpoint")]
279+
public static partial void ExecutingEndpoint(ILogger logger, Endpoint endpointName);
280+
281+
[LoggerMessage(5, LogLevel.Information, "The endpoint '{EndpointName}' has been executed without running additional middleware.", EventName = "ExecutedEndpoint")]
282+
public static partial void ExecutedEndpoint(ILogger logger, Endpoint endpointName);
283+
284+
[LoggerMessage(6, LogLevel.Information, "The endpoint '{EndpointName}' is being short circuited without running additional middleware or producing a response.", EventName = "ShortCircuitedEndpoint")]
285+
public static partial void ShortCircuitedEndpoint(ILogger logger, Endpoint endpointName);
184286
}
185287
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
#nullable enable
22
Microsoft.AspNetCore.Routing.RouteHandlerServices
33
static Microsoft.AspNetCore.Routing.RouteHandlerServices.Map(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler, System.Collections.Generic.IEnumerable<string!>! httpMethods, System.Func<System.Reflection.MethodInfo!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult!>! populateMetadata, System.Func<System.Delegate!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions!, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult!>! createRequestDelegate) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
4+
Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions
5+
Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions
6+
static Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions.ShortCircuit(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int? statusCode = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
7+
static Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions.MapShortCircuit(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! builder, int statusCode, params string![]! routePrefixes) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
48
static Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions.AddRoutingCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 Microsoft.AspNetCore.Routing.ShortCircuit;
5+
6+
namespace Microsoft.AspNetCore.Builder;
7+
8+
/// <summary>
9+
/// Short circuit extension methods for <see cref="IEndpointConventionBuilder"/>.
10+
/// </summary>
11+
public static class RouteShortCircuitEndpointConventionBuilderExtensions
12+
{
13+
private static readonly ShortCircuitMetadata _200ShortCircuitMetadata = new ShortCircuitMetadata(200);
14+
private static readonly ShortCircuitMetadata _401ShortCircuitMetadata = new ShortCircuitMetadata(401);
15+
private static readonly ShortCircuitMetadata _404ShortCircuitMetadata = new ShortCircuitMetadata(404);
16+
private static readonly ShortCircuitMetadata _nullShortCircuitMetadata = new ShortCircuitMetadata(null);
17+
18+
/// <summary>
19+
/// Short circuit the endpoint(s).
20+
/// The execution of the endpoint will happen in UseRouting middleware instead of UseEndpoint.
21+
/// </summary>
22+
/// <param name="builder">The endpoint convention builder.</param>
23+
/// <param name="statusCode">The status code to set in the response.</param>
24+
/// <returns>The original convention builder parameter.</returns>
25+
public static IEndpointConventionBuilder ShortCircuit(this IEndpointConventionBuilder builder, int? statusCode = null)
26+
{
27+
var metadata = statusCode switch
28+
{
29+
200 => _200ShortCircuitMetadata,
30+
401 => _401ShortCircuitMetadata,
31+
404 => _404ShortCircuitMetadata,
32+
null => _nullShortCircuitMetadata,
33+
_ => new ShortCircuitMetadata(statusCode)
34+
};
35+
36+
builder.Add(b => b.Metadata.Add(metadata));
37+
return builder;
38+
}
39+
}

0 commit comments

Comments
 (0)