Skip to content

Commit 5fa80eb

Browse files
Add support for route handler filter factories (#40667)
* Add support for route handler filter factories * Address feedback from peer review * Apply suggestions from code review Co-authored-by: David Fowler <[email protected]> * Fix XML comment for RouteHandlerFilterDelegate * React to feedback from API review * Fix up RouteHandlerContext instantiation * Address more feedback from peer review * Remove RouteHandlerContext passing * Remove old RouteHandlerFilterFactories implementation * Fix up RouteHandlerContext references Co-authored-by: David Fowler <[email protected]>
1 parent 95d0fef commit 5fa80eb

15 files changed

+440
-136
lines changed

src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ namespace Microsoft.AspNetCore.Http;
99
public interface IRouteHandlerFilter
1010
{
1111
/// <summary>
12-
/// Implements the core logic associated with the filter given a <see cref="RouteHandlerFilterContext"/>
12+
/// Implements the core logic associated with the filter given a <see cref="RouteHandlerInvocationContext"/>
1313
/// and the next filter to call in the pipeline.
1414
/// </summary>
15-
/// <param name="context">The <see cref="RouteHandlerFilterContext"/> associated with the current request/response.</param>
15+
/// <param name="context">The <see cref="RouteHandlerInvocationContext"/> associated with the current request/response.</param>
1616
/// <param name="next">The next filter in the pipeline.</param>
1717
/// <returns>An awaitable result of calling the handler and apply
1818
/// any modifications made by filters in the pipeline.</returns>
19-
ValueTask<object?> InvokeAsync(RouteHandlerFilterContext context, Func<RouteHandlerFilterContext, ValueTask<object?>> next);
19+
ValueTask<object?> InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next);
2020
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
#nullable enable
22
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string!
33
Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata<T>() -> T!
4-
Microsoft.AspNetCore.Http.RouteHandlerFilterContext.RouteHandlerFilterContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! parameters) -> void
5-
Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerFilterContext! context, System.Func<Microsoft.AspNetCore.Http.RouteHandlerFilterContext!, System.Threading.Tasks.ValueTask<object?>>! next) -> System.Threading.Tasks.ValueTask<object?>
4+
Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask<object?>
65
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
76
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
7+
Microsoft.AspNetCore.Http.RouteHandlerContext
8+
Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection!
9+
Microsoft.AspNetCore.Http.RouteHandlerContext.MethodInfo.get -> System.Reflection.MethodInfo!
10+
Microsoft.AspNetCore.Http.RouteHandlerContext.RouteHandlerContext(System.Reflection.MethodInfo! methodInfo, Microsoft.AspNetCore.Http.EndpointMetadataCollection! endpointMetadata) -> void
11+
Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate
12+
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext
13+
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
14+
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.Parameters.get -> System.Collections.Generic.IList<object?>!
15+
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.RouteHandlerInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! parameters) -> void
816
Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(Microsoft.AspNetCore.Routing.RouteValueDictionary? dictionary) -> void
917
Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object?>>? values) -> void
1018
Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string?>>? values) -> void
1119
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
1220
Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata
13-
Microsoft.AspNetCore.Http.RouteHandlerFilterContext
14-
Microsoft.AspNetCore.Http.RouteHandlerFilterContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
15-
Microsoft.AspNetCore.Http.RouteHandlerFilterContext.Parameters.get -> System.Collections.Generic.IList<object?>!
1621
Microsoft.AspNetCore.Http.IRouteHandlerFilter
1722
Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata
1823
Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata.Description.get -> string!
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.Reflection;
5+
6+
namespace Microsoft.AspNetCore.Http;
7+
8+
/// <summary>
9+
/// Represents the information accessible via the route handler filter
10+
/// API when the user is constructing a new route handler.
11+
/// </summary>
12+
public sealed class RouteHandlerContext
13+
{
14+
/// <summary>
15+
/// Creates a new instance of the <see cref="RouteHandlerContext"/>.
16+
/// </summary>
17+
/// <param name="methodInfo">The <see cref="MethodInfo"/> associated with the route handler of the current request.</param>
18+
/// <param name="endpointMetadata">The <see cref="EndpointMetadataCollection"/> associated with the endpoint the filter is targeting.</param>
19+
public RouteHandlerContext(MethodInfo methodInfo, EndpointMetadataCollection endpointMetadata)
20+
{
21+
MethodInfo = methodInfo;
22+
EndpointMetadata = endpointMetadata;
23+
}
24+
25+
/// <summary>
26+
/// The <see cref="MethodInfo"/> associated with the current route handler.
27+
/// </summary>
28+
public MethodInfo MethodInfo { get; }
29+
30+
/// <summary>
31+
/// The <see cref="EndpointMetadataCollection"/> associated with the current endpoint.
32+
/// </summary>
33+
public EndpointMetadataCollection EndpointMetadata { get; }
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
/// <summary>
7+
/// A delegate that is applied as a filter on a route handler.
8+
/// </summary>
9+
/// <param name="context">The <see cref="RouteHandlerInvocationContext"/> associated with the current request.</param>
10+
/// <returns>
11+
/// A <see cref="ValueTask"/> result of calling the handler and applying any modifications made by filters in the pipeline.
12+
/// </returns>
13+
public delegate ValueTask<object?> RouteHandlerFilterDelegate(RouteHandlerInvocationContext context);

src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs renamed to src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ namespace Microsoft.AspNetCore.Http;
77
/// Provides an abstraction for wrapping the <see cref="HttpContext"/> and parameters
88
/// provided to a route handler.
99
/// </summary>
10-
public class RouteHandlerFilterContext
10+
public sealed class RouteHandlerInvocationContext
1111
{
1212
/// <summary>
13-
/// Creates a new instance of the <see cref="RouteHandlerFilterContext"/> for a given request.
13+
/// Creates a new instance of the <see cref="RouteHandlerInvocationContext"/> for a given request.
1414
/// </summary>
1515
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
1616
/// <param name="parameters">A list of parameters provided in the current request.</param>
17-
public RouteHandlerFilterContext(HttpContext httpContext, params object[] parameters)
17+
public RouteHandlerInvocationContext(HttpContext httpContext, params object[] parameters)
1818
{
1919
HttpContext = httpContext;
2020
Parameters = parameters;
@@ -28,7 +28,7 @@ public RouteHandlerFilterContext(HttpContext httpContext, params object[] parame
2828
/// <summary>
2929
/// A list of parameters provided in the current request to the filter.
3030
/// <remarks>
31-
/// This list is not read-only to premit modifying of existing parameters by filters.
31+
/// This list is not read-only to permit modifying of existing parameters by filters.
3232
/// </remarks>
3333
/// </summary>
3434
public IList<object?> Parameters { get; }

src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList<System.Func<Microsoft.AspNetCore.Http.RouteHandlerContext!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!>!>?
3+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void
24
Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions
35
static Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions.ConfigureRouteHandlerJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Http.Json.JsonOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
4-
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilters.get -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.IRouteHandlerFilter!>?
5-
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilters.init -> void
66
Microsoft.AspNetCore.Http.EndpointDescriptionAttribute
77
Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.EndpointDescriptionAttribute(string! description) -> void
88
Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.Description.get -> string!

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

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ public static partial class RequestDelegateFactory
7979
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
8080
private static readonly UnaryExpression TempSourceStringIsNotNullOrEmptyExpr = Expression.Not(Expression.Call(StringIsNullOrEmptyMethod, TempSourceStringExpr));
8181

82-
private static readonly ConstructorInfo RouteHandlerFilterContextConstructor = typeof(RouteHandlerFilterContext).GetConstructor(new[] { typeof(HttpContext), typeof(object[]) })!;
83-
private static readonly ParameterExpression FilterContextExpr = Expression.Parameter(typeof(RouteHandlerFilterContext), "context");
84-
private static readonly MemberExpression FilterContextParametersExpr = Expression.Property(FilterContextExpr, typeof(RouteHandlerFilterContext).GetProperty(nameof(RouteHandlerFilterContext.Parameters))!);
85-
private static readonly MemberExpression FilterContextHttpContextExpr = Expression.Property(FilterContextExpr, typeof(RouteHandlerFilterContext).GetProperty(nameof(RouteHandlerFilterContext.HttpContext))!);
82+
private static readonly ConstructorInfo RouteHandlerInvocationContextConstructor = typeof(RouteHandlerInvocationContext).GetConstructor(new[] { typeof(HttpContext), typeof(object[]) })!;
83+
private static readonly ParameterExpression FilterContextExpr = Expression.Parameter(typeof(RouteHandlerInvocationContext), "context");
84+
private static readonly MemberExpression FilterContextParametersExpr = Expression.Property(FilterContextExpr, typeof(RouteHandlerInvocationContext).GetProperty(nameof(RouteHandlerInvocationContext.Parameters))!);
85+
private static readonly MemberExpression FilterContextHttpContextExpr = Expression.Property(FilterContextExpr, typeof(RouteHandlerInvocationContext).GetProperty(nameof(RouteHandlerInvocationContext.HttpContext))!);
8686
private static readonly MemberExpression FilterContextHttpContextResponseExpr = Expression.Property(FilterContextHttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Response))!);
8787
private static readonly MemberExpression FilterContextHttpContextStatusCodeExpr = Expression.Property(FilterContextHttpContextResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!);
88-
private static readonly ParameterExpression InvokedFilterContextExpr = Expression.Parameter(typeof(RouteHandlerFilterContext), "filterContext");
88+
private static readonly ParameterExpression InvokedFilterContextExpr = Expression.Parameter(typeof(RouteHandlerInvocationContext), "filterContext");
8989

9090
private static readonly string[] DefaultAcceptsContentType = new[] { "application/json" };
9191
private static readonly string[] FormFileContentType = new[] { "multipart/form-data" };
@@ -166,7 +166,7 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
166166
RouteParameters = options?.RouteParameterNames?.ToList(),
167167
ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false,
168168
DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false,
169-
Filters = options?.RouteHandlerFilters?.ToList()
169+
Filters = options?.RouteHandlerFilterFactories?.ToList()
170170
};
171171

172172
private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext)
@@ -196,15 +196,15 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
196196
if (factoryContext.Filters is { Count: > 0 })
197197
{
198198
var filterPipeline = CreateFilterPipeline(methodInfo, targetExpression, factoryContext);
199-
Expression<Func<RouteHandlerFilterContext, ValueTask<object?>>> invokePipeline = (context) => filterPipeline(context);
199+
Expression<Func<RouteHandlerInvocationContext, ValueTask<object?>>> invokePipeline = (context) => filterPipeline(context);
200200
returnType = typeof(ValueTask<object?>);
201-
// var filterContext = new RouteHandlerFilterContext(httpContext, new[] { (object)name_local, (object)int_local });
201+
// var filterContext = new RouteHandlerInvocationContext(httpContext, new[] { (object)name_local, (object)int_local });
202202
// invokePipeline.Invoke(filterContext);
203203
factoryContext.MethodCall = Expression.Block(
204204
new[] { InvokedFilterContextExpr },
205205
Expression.Assign(
206206
InvokedFilterContextExpr,
207-
Expression.New(RouteHandlerFilterContextConstructor,
207+
Expression.New(RouteHandlerInvocationContextConstructor,
208208
new Expression[] { HttpContextExpr, Expression.NewArrayInit(typeof(object), factoryContext.BoxedArgs) })),
209209
Expression.Invoke(invokePipeline, InvokedFilterContextExpr)
210210
);
@@ -222,13 +222,13 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
222222
return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext);
223223
}
224224

225-
private static Func<RouteHandlerFilterContext, ValueTask<object?>> CreateFilterPipeline(MethodInfo methodInfo, Expression? target, FactoryContext factoryContext)
225+
private static RouteHandlerFilterDelegate CreateFilterPipeline(MethodInfo methodInfo, Expression? target, FactoryContext factoryContext)
226226
{
227227
Debug.Assert(factoryContext.Filters is not null);
228228
// httpContext.Response.StatusCode >= 400
229229
// ? Task.CompletedTask
230230
// : handler((string)context.Parameters[0], (int)context.Parameters[1])
231-
var filteredInvocation = Expression.Lambda<Func<RouteHandlerFilterContext, ValueTask<object?>>>(
231+
var filteredInvocation = Expression.Lambda<RouteHandlerFilterDelegate>(
232232
Expression.Condition(
233233
Expression.GreaterThanOrEqual(FilterContextHttpContextStatusCodeExpr, Expression.Constant(400)),
234234
CompletedValueTaskExpr,
@@ -240,12 +240,16 @@ target is null
240240
: Expression.Call(target, methodInfo, factoryContext.ContextArgAccess))
241241
)),
242242
FilterContextExpr).Compile();
243+
var routeHandlerContext = new RouteHandlerContext(
244+
methodInfo,
245+
new EndpointMetadataCollection(factoryContext.Metadata));
243246

244247
for (var i = factoryContext.Filters.Count - 1; i >= 0; i--)
245248
{
246-
var currentFilter = factoryContext.Filters![i];
249+
var currentFilterFactory = factoryContext.Filters[i];
247250
var nextFilter = filteredInvocation;
248-
filteredInvocation = (RouteHandlerFilterContext context) => currentFilter.InvokeAsync(context, nextFilter);
251+
var currentFilter = currentFilterFactory(routeHandlerContext, nextFilter);
252+
filteredInvocation = (RouteHandlerInvocationContext context) => currentFilter(context);
249253

250254
}
251255
return filteredInvocation;
@@ -264,7 +268,7 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory
264268
{
265269
args[i] = CreateArgument(parameters[i], factoryContext);
266270
// Register expressions containing the boxed and unboxed variants
267-
// of the route handler's arguments for use in RouteHandlerFilterContext
271+
// of the route handler's arguments for use in RouteHandlerInvocationContext
268272
// construction and route handler invocation.
269273
// (string)context.Parameters[0];
270274
factoryContext.ContextArgAccess.Add(
@@ -1693,7 +1697,7 @@ private class FactoryContext
16931697
public List<Expression> ContextArgAccess { get; } = new();
16941698
public Expression? MethodCall { get; set; }
16951699
public List<Expression> BoxedArgs { get; } = new();
1696-
public List<IRouteHandlerFilter>? Filters { get; init; }
1700+
public List<Func<RouteHandlerContext, RouteHandlerFilterDelegate, RouteHandlerFilterDelegate>>? Filters { get; init; }
16971701
}
16981702

16991703
private static class RequestDelegateFactoryConstants

src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ public sealed class RequestDelegateFactoryOptions
3535
/// <summary>
3636
/// The list of filters that must run in the pipeline for a given route handler.
3737
/// </summary>
38-
public IReadOnlyList<IRouteHandlerFilter>? RouteHandlerFilters { get; init; }
38+
public IReadOnlyList<Func<RouteHandlerContext, RouteHandlerFilterDelegate, RouteHandlerFilterDelegate>>? RouteHandlerFilterFactories { get; init; }
3939
}

0 commit comments

Comments
 (0)