Skip to content
6 changes: 3 additions & 3 deletions src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ namespace Microsoft.AspNetCore.Http;
public interface IRouteHandlerFilter
{
/// <summary>
/// Implements the core logic associated with the filter given a <see cref="RouteHandlerFilterContext"/>
/// Implements the core logic associated with the filter given a <see cref="RouteHandlerInvocationContext"/>
/// and the next filter to call in the pipeline.
/// </summary>
/// <param name="context">The <see cref="RouteHandlerFilterContext"/> associated with the current request/response.</param>
/// <param name="context">The <see cref="RouteHandlerInvocationContext"/> associated with the current request/response.</param>
/// <param name="next">The next filter in the pipeline.</param>
/// <returns>An awaitable result of calling the handler and apply
/// any modifications made by filters in the pipeline.</returns>
ValueTask<object?> InvokeAsync(RouteHandlerFilterContext context, Func<RouteHandlerFilterContext, ValueTask<object?>> next);
ValueTask<object?> InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we feel about adding a RouteHandlerContext parameter? We can try to approve this over email if we like it.

Suggested change
ValueTask<object?> InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next);
ValueTask<object?> InvokeAsync(RouteHandlerContext routeContext, RouteHandlerInvocationContext invocationContext, RouteHandlerFilterDelegate next);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? This should e a constructor argument, like middleware has the magic next no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does middleware pass the magic next?

The implementation I just pushed passes the RouteHandlerContext as an argument to the object factory generated when the user utilizes the AddFilter<TFilterType>() overload.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up: #40724

}
15 changes: 10 additions & 5 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
#nullable enable
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string!
Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata<T>() -> T!
Microsoft.AspNetCore.Http.RouteHandlerFilterContext.RouteHandlerFilterContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! parameters) -> void
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?>
Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask<object?>
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
Microsoft.AspNetCore.Http.RouteHandlerContext
Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection!
Microsoft.AspNetCore.Http.RouteHandlerContext.MethodInfo.get -> System.Reflection.MethodInfo!
Microsoft.AspNetCore.Http.RouteHandlerContext.RouteHandlerContext(System.Reflection.MethodInfo! methodInfo, Microsoft.AspNetCore.Http.EndpointMetadataCollection! endpointMetadata) -> void
Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.Parameters.get -> System.Collections.Generic.IList<object?>!
Microsoft.AspNetCore.Http.RouteHandlerInvocationContext.RouteHandlerInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! parameters) -> void
Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(Microsoft.AspNetCore.Routing.RouteValueDictionary? dictionary) -> void
Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object?>>? values) -> void
Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string?>>? values) -> void
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata
Microsoft.AspNetCore.Http.RouteHandlerFilterContext
Microsoft.AspNetCore.Http.RouteHandlerFilterContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
Microsoft.AspNetCore.Http.RouteHandlerFilterContext.Parameters.get -> System.Collections.Generic.IList<object?>!
Microsoft.AspNetCore.Http.IRouteHandlerFilter
Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata
Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata.Description.get -> string!
Expand Down
34 changes: 34 additions & 0 deletions src/Http/Http.Abstractions/src/RouteHandlerContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Represents the information accessible via the route handler filter
/// API when the user is constructing a new route handler.
/// </summary>
public sealed class RouteHandlerContext
{
/// <summary>
/// Creates a new instance of the <see cref="RouteHandlerContext"/>.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/> associated with the route handler of the current request.</param>
/// <param name="endpointMetadata">The <see cref="EndpointMetadataCollection"/> associated with the endpoint the filter is targeting.</param>
public RouteHandlerContext(MethodInfo methodInfo, EndpointMetadataCollection endpointMetadata)
{
MethodInfo = methodInfo;
EndpointMetadata = endpointMetadata;
}

/// <summary>
/// The <see cref="MethodInfo"/> associated with the current route handler.
/// </summary>
public MethodInfo MethodInfo { get; }

/// <summary>
/// The <see cref="EndpointMetadataCollection"/> associated with the current endpoint.
/// </summary>
public EndpointMetadataCollection EndpointMetadata { get; }
}
13 changes: 13 additions & 0 deletions src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// A delegate that is applied as a filter on a route handler.
/// </summary>
/// <param name="context">The <see cref="RouteHandlerInvocationContext"/> associated with the current request.</param>
/// <returns>
/// A <see cref="ValueTask"/> result of calling the handler and applying any modifications made by filters in the pipeline.
/// </returns>
public delegate ValueTask<object?> RouteHandlerFilterDelegate(RouteHandlerInvocationContext context);
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ namespace Microsoft.AspNetCore.Http;
/// Provides an abstraction for wrapping the <see cref="HttpContext"/> and parameters
/// provided to a route handler.
/// </summary>
public class RouteHandlerFilterContext
public sealed class RouteHandlerInvocationContext
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't seal this one. We'll end up un sealing it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this during API review. Unsealing doesn't have a cost, but sealing does so we are taking the more flexible option to start.

If we ship the parameters change in preview4, it shouldn't be too bad.

{
/// <summary>
/// Creates a new instance of the <see cref="RouteHandlerFilterContext"/> for a given request.
/// Creates a new instance of the <see cref="RouteHandlerInvocationContext"/> for a given request.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="parameters">A list of parameters provided in the current request.</param>
public RouteHandlerFilterContext(HttpContext httpContext, params object[] parameters)
public RouteHandlerInvocationContext(HttpContext httpContext, params object[] parameters)
{
HttpContext = httpContext;
Parameters = parameters;
Expand All @@ -28,7 +28,7 @@ public RouteHandlerFilterContext(HttpContext httpContext, params object[] parame
/// <summary>
/// A list of parameters provided in the current request to the filter.
/// <remarks>
/// This list is not read-only to premit modifying of existing parameters by filters.
/// This list is not read-only to permit modifying of existing parameters by filters.
/// </remarks>
/// </summary>
public IList<object?> Parameters { get; }
Expand Down
4 changes: 2 additions & 2 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#nullable enable
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList<System.Func<Microsoft.AspNetCore.Http.RouteHandlerContext!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!>!>?
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void
Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions
static Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions.ConfigureRouteHandlerJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Http.Json.JsonOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilters.get -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.IRouteHandlerFilter!>?
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilters.init -> void
Microsoft.AspNetCore.Http.EndpointDescriptionAttribute
Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.EndpointDescriptionAttribute(string! description) -> void
Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.Description.get -> string!
Expand Down
34 changes: 19 additions & 15 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ public static partial class RequestDelegateFactory
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
private static readonly UnaryExpression TempSourceStringIsNotNullOrEmptyExpr = Expression.Not(Expression.Call(StringIsNullOrEmptyMethod, TempSourceStringExpr));

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

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

private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext)
Expand Down Expand Up @@ -196,15 +196,15 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
if (factoryContext.Filters is { Count: > 0 })
{
var filterPipeline = CreateFilterPipeline(methodInfo, targetExpression, factoryContext);
Expression<Func<RouteHandlerFilterContext, ValueTask<object?>>> invokePipeline = (context) => filterPipeline(context);
Expression<Func<RouteHandlerInvocationContext, ValueTask<object?>>> invokePipeline = (context) => filterPipeline(context);
returnType = typeof(ValueTask<object?>);
// var filterContext = new RouteHandlerFilterContext(httpContext, new[] { (object)name_local, (object)int_local });
// var filterContext = new RouteHandlerInvocationContext(httpContext, new[] { (object)name_local, (object)int_local });
// invokePipeline.Invoke(filterContext);
factoryContext.MethodCall = Expression.Block(
new[] { InvokedFilterContextExpr },
Expression.Assign(
InvokedFilterContextExpr,
Expression.New(RouteHandlerFilterContextConstructor,
Expression.New(RouteHandlerInvocationContextConstructor,
new Expression[] { HttpContextExpr, Expression.NewArrayInit(typeof(object), factoryContext.BoxedArgs) })),
Expression.Invoke(invokePipeline, InvokedFilterContextExpr)
);
Expand All @@ -222,13 +222,13 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext);
}

private static Func<RouteHandlerFilterContext, ValueTask<object?>> CreateFilterPipeline(MethodInfo methodInfo, Expression? target, FactoryContext factoryContext)
private static RouteHandlerFilterDelegate CreateFilterPipeline(MethodInfo methodInfo, Expression? target, FactoryContext factoryContext)
{
Debug.Assert(factoryContext.Filters is not null);
// httpContext.Response.StatusCode >= 400
// ? Task.CompletedTask
// : handler((string)context.Parameters[0], (int)context.Parameters[1])
var filteredInvocation = Expression.Lambda<Func<RouteHandlerFilterContext, ValueTask<object?>>>(
var filteredInvocation = Expression.Lambda<RouteHandlerFilterDelegate>(
Expression.Condition(
Expression.GreaterThanOrEqual(FilterContextHttpContextStatusCodeExpr, Expression.Constant(400)),
CompletedValueTaskExpr,
Expand All @@ -240,12 +240,16 @@ target is null
: Expression.Call(target, methodInfo, factoryContext.ContextArgAccess))
)),
FilterContextExpr).Compile();
var routeHandlerContext = new RouteHandlerContext(
methodInfo,
new EndpointMetadataCollection(factoryContext.Metadata));

for (var i = factoryContext.Filters.Count - 1; i >= 0; i--)
{
var currentFilter = factoryContext.Filters![i];
var currentFilterFactory = factoryContext.Filters[i];
var nextFilter = filteredInvocation;
filteredInvocation = (RouteHandlerFilterContext context) => currentFilter.InvokeAsync(context, nextFilter);
var currentFilter = currentFilterFactory(routeHandlerContext, nextFilter);
filteredInvocation = (RouteHandlerInvocationContext context) => currentFilter(context);

}
return filteredInvocation;
Expand All @@ -264,7 +268,7 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory
{
args[i] = CreateArgument(parameters[i], factoryContext);
// Register expressions containing the boxed and unboxed variants
// of the route handler's arguments for use in RouteHandlerFilterContext
// of the route handler's arguments for use in RouteHandlerInvocationContext
// construction and route handler invocation.
// (string)context.Parameters[0];
factoryContext.ContextArgAccess.Add(
Expand Down Expand Up @@ -1693,7 +1697,7 @@ private class FactoryContext
public List<Expression> ContextArgAccess { get; } = new();
public Expression? MethodCall { get; set; }
public List<Expression> BoxedArgs { get; } = new();
public List<IRouteHandlerFilter>? Filters { get; init; }
public List<Func<RouteHandlerContext, RouteHandlerFilterDelegate, RouteHandlerFilterDelegate>>? Filters { get; init; }
}

private static class RequestDelegateFactoryConstants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ public sealed class RequestDelegateFactoryOptions
/// <summary>
/// The list of filters that must run in the pipeline for a given route handler.
/// </summary>
public IReadOnlyList<IRouteHandlerFilter>? RouteHandlerFilters { get; init; }
public IReadOnlyList<Func<RouteHandlerContext, RouteHandlerFilterDelegate, RouteHandlerFilterDelegate>>? RouteHandlerFilterFactories { get; init; }
}
Loading