From a5cb7e416cca38760484dac60f0c39b6987f35d4 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 10 Mar 2022 18:28:48 -0800 Subject: [PATCH 01/10] Add support for route handler filter factories --- .../src/IRouteHandlerFilter.cs | 4 +- .../src/PublicAPI.Unshipped.txt | 6 +- .../src/RouteHandlerFilterContext.cs | 11 +- .../src/RouteHandlerFilterDelegate.cs | 13 ++ .../src/PublicAPI.Unshipped.txt | 4 +- .../src/RequestDelegateFactory.cs | 17 +- .../src/RequestDelegateFactoryOptions.cs | 4 +- .../test/RequestDelegateFactoryTests.cs | 200 +++++++++++------- .../src/Builder/DelegateRouteHandlerFilter.cs | 19 -- .../Builder/EndpointRouteBuilderExtensions.cs | 2 +- .../src/Builder/RouteHandlerBuilder.cs | 5 +- .../Builder/RouteHandlerFilterExtensions.cs | 31 ++- src/Http/Routing/src/PublicAPI.Unshipped.txt | 3 +- ...ndlerEndpointRouteBuilderExtensionsTest.cs | 137 +++++++++++- 14 files changed, 333 insertions(+), 123 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs delete mode 100644 src/Http/Routing/src/Builder/DelegateRouteHandlerFilter.cs diff --git a/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs b/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs index 4d3e583eaa5d..0897394b95e9 100644 --- a/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs +++ b/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http.Abstractions; + namespace Microsoft.AspNetCore.Http; /// @@ -16,5 +18,5 @@ public interface IRouteHandlerFilter /// The next filter in the pipeline. /// An awaitable result of calling the handler and apply /// any modifications made by filters in the pipeline. - ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next); + ValueTask InvokeAsync(RouteHandlerFilterContext context, RouteHandlerFilterDelegate next); } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index bee1f68cba6d..e7724abff1e8 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,10 +1,12 @@ #nullable enable *REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string! +Microsoft.AspNetCore.Http.Abstractions.RouteHandlerFilterDelegate Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> 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>! next) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerFilterContext! context, Microsoft.AspNetCore.Http.Abstractions.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? +Microsoft.AspNetCore.Http.RouteHandlerFilterContext.RouteHandlerFilterContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.IServiceProvider! serviceProvider, params object![]! parameters) -> void +Microsoft.AspNetCore.Http.RouteHandlerFilterContext.ServiceProvider.get -> System.IServiceProvider! Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(Microsoft.AspNetCore.Routing.RouteValueDictionary? dictionary) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable>? values) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable>? values) -> void diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs b/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs index 558d97cbd06b..0c42b5a7179e 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs @@ -13,11 +13,13 @@ public class RouteHandlerFilterContext /// Creates a new instance of the for a given request. /// /// The associated with the current request. + /// The associated with the current request. /// A list of parameters provided in the current request. - public RouteHandlerFilterContext(HttpContext httpContext, params object[] parameters) + public RouteHandlerFilterContext(HttpContext httpContext, IServiceProvider serviceProvider, params object[] parameters) { HttpContext = httpContext; Parameters = parameters; + ServiceProvider = serviceProvider; } /// @@ -28,8 +30,13 @@ public RouteHandlerFilterContext(HttpContext httpContext, params object[] parame /// /// A list of parameters provided in the current request to the filter. /// - /// 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. /// /// public IList Parameters { get; } + + /// + /// The associated with the current request. + /// + public IServiceProvider ServiceProvider { get; } } diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs new file mode 100644 index 000000000000..a3fd423f6e3e --- /dev/null +++ b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs @@ -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.Abstractions; + +/// +/// A delegate that is applied as a filter on a route handler. +/// +/// The associated with the current request. +/// +/// An awaitable result of calling the handler and applying any modifications made by filters in the pipeline. +/// +public delegate ValueTask RouteHandlerFilterDelegate(RouteHandlerFilterContext context); diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 1d4c624f9113..faeb31077dbf 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? +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! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilters.get -> System.Collections.Generic.IReadOnlyList? -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilters.init -> void diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 3807ea2c342d..deb502f9a40d 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -10,6 +10,7 @@ using System.Security.Claims; using System.Text; using System.Text.Json; +using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -79,7 +80,7 @@ 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 ConstructorInfo RouteHandlerFilterContextConstructor = typeof(RouteHandlerFilterContext).GetConstructor(new[] { typeof(HttpContext), typeof(IServiceProvider), 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))!); @@ -166,7 +167,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 CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext) @@ -205,7 +206,7 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions Expression.Assign( InvokedFilterContextExpr, Expression.New(RouteHandlerFilterContextConstructor, - new Expression[] { HttpContextExpr, Expression.NewArrayInit(typeof(object), factoryContext.BoxedArgs) })), + new Expression[] { HttpContextExpr, RequestServicesExpr, Expression.NewArrayInit(typeof(object), factoryContext.BoxedArgs) })), Expression.Invoke(invokePipeline, InvokedFilterContextExpr) ); } @@ -222,13 +223,13 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext); } - private static Func> 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>>( + var filteredInvocation = Expression.Lambda( Expression.Condition( Expression.GreaterThanOrEqual(FilterContextHttpContextStatusCodeExpr, Expression.Constant(400)), CompletedValueTaskExpr, @@ -243,9 +244,9 @@ target is null 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); + filteredInvocation = (RouteHandlerFilterContext context) => currentFilterFactory(methodInfo, nextFilter)(context); } return filteredInvocation; @@ -1693,7 +1694,7 @@ private class FactoryContext public List ContextArgAccess { get; } = new(); public Expression? MethodCall { get; set; } public List BoxedArgs { get; } = new(); - public List? Filters { get; init; } + public List>? Filters { get; init; } } private static class RequestDelegateFactoryConstants diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 870c2a06158e..19e431eee103 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -1,6 +1,8 @@ // 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; +using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Logging; @@ -35,5 +37,5 @@ public sealed class RequestDelegateFactoryOptions /// /// The list of filters that must run in the pipeline for a given route handler. /// - public IReadOnlyList? RouteHandlerFilters { get; init; } + public IReadOnlyList>? RouteHandlerFilterFactories { get; init; } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 1af56e02660e..b7fc626b3806 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -19,6 +19,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.Metadata; @@ -4216,7 +4217,14 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilters = new List() { new ModifyStringArgumentFilter() } + RouteHandlerFilterFactories = new List>() + { + (methodInfo, next) => async (context) => + { + context.Parameters[0] = context.Parameters[0] != null ? $"{((string)context.Parameters[0]!)}Prefix" : "NULL"; + return await next(context); + } + } }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -4243,7 +4251,16 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilters = new List() { new ProvideCustomErrorMessageFilter() } + RouteHandlerFilterFactories = new List>() { + (methodInfo, next) => async (context) => + { + if (context.HttpContext.Response.StatusCode == 400) + { + return Results.Problem("New response", statusCode: 400); + } + return await next(context); + } + } }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -4280,7 +4297,22 @@ string HelloName(string name, int age) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilters = new List() { new ModifyIntArgumentFilter(), new LogArgumentsFilter(Log) } + RouteHandlerFilterFactories = new List>() + { + (methodInfo, next) => async (context) => + { + context.Parameters[1] = ((int)context.Parameters[1]!) + 2; + return await next(context); + }, + (methodInfo, next) => async (context) => + { + foreach (var parameter in context.Parameters) + { + Log(parameter!.ToString() ?? "no arg"); + } + return await next(context); + } + } }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -4291,6 +4323,54 @@ string HelloName(string name, int age) Assert.Equal(2, loggerInvoked); } + [Fact] + public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatUsesMethodInfo() + { + // Arrange + string HelloName(string name) + { + return $"Hello, {name}!."; + }; + + var httpContext = CreateHttpContext(); + + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName" + }); + + // Act + var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() + { + RouteHandlerFilterFactories = new List>() + { + (methodInfo, next) => + { + var parameters = methodInfo.GetParameters(); + var isInt = parameters.Length == 2 && parameters[1].ParameterType == typeof(int); + return async (context) => + { + if (isInt) + { + context.Parameters[1] = ((int)context.Parameters[1]!) + 2; + return await next(context); + } + return "Is not an int."; + }; + }, + } + }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); + + // Assert + var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal("Is not an int.", responseBody); + } + [Fact] public async Task RequestDelegateFactory_CanInvokeSingleEndpointFilter_ThatModifiesBodyParameter() { @@ -4316,7 +4396,16 @@ string PrintTodo(Todo todo) // Act var factoryResult = RequestDelegateFactory.Create(PrintTodo, new RequestDelegateFactoryOptions() { - RouteHandlerFilters = new List() { new ModifyTodoArgumentFilter() } + RouteHandlerFilterFactories = new List>() + { + (methodInfo, next) => async (context) => + { + Todo originalTodo = (Todo)context.Parameters[0]!; + originalTodo!.IsComplete = !originalTodo.IsComplete; + context.Parameters[0] = originalTodo; + return await next(context); + } + } }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -4348,7 +4437,18 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilters = new List() { new ModifyStringResultFilter() } + RouteHandlerFilterFactories = new List>() + { + (methodInfo, next) => async (context) => + { + var previousResult = await next(context); + if (previousResult is string stringResult) + { + return stringResult.ToUpperInvariant(); + } + return previousResult; + } + } }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -4380,7 +4480,23 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilters = new List() { new ModifyStringResultFilter(), new ModifyStringArgumentFilter() } + RouteHandlerFilterFactories = new List>() + { + (methodInfo, next) => async (context) => + { + var previousResult = await next(context); + if (previousResult is string stringResult) + { + return stringResult.ToUpperInvariant(); + } + return previousResult; + }, + (methodInfo, next) => async (context) => + { + context.Parameters[0] = context.Parameters[0] != null ? $"{((string)context.Parameters[0]!)}Prefix" : "NULL"; + return await next(context); + } + } }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -4749,78 +4865,6 @@ public TlsConnectionFeature(X509Certificate2 clientCertificate) throw new NotImplementedException(); } } - - private class ModifyStringArgumentFilter : IRouteHandlerFilter - { - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next) - { - context.Parameters[0] = context.Parameters[0] != null ? $"{((string)context.Parameters[0]!)}Prefix" : "NULL"; - return await next(context); - } - } - - private class ModifyIntArgumentFilter : IRouteHandlerFilter - { - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next) - { - context.Parameters[1] = ((int)context.Parameters[1]!) + 2; - return await next(context); - } - } - - private class ModifyTodoArgumentFilter : IRouteHandlerFilter - { - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next) - { - Todo originalTodo = (Todo)context.Parameters[0]!; - originalTodo!.IsComplete = !originalTodo.IsComplete; - context.Parameters[0] = originalTodo; - return await next(context); - } - } - - private class ProvideCustomErrorMessageFilter : IRouteHandlerFilter - { - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next) - { - if (context.HttpContext.Response.StatusCode == 400) - { - return Results.Problem("New response", statusCode: 400); - } - return await next(context); - } - } - - private class LogArgumentsFilter : IRouteHandlerFilter - { - private Action _logger; - - public LogArgumentsFilter(Action logger) - { - _logger = logger; - } - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next) - { - foreach (var parameter in context.Parameters) - { - _logger(parameter!.ToString() ?? "no arg"); - } - return await next(context); - } - } - - private class ModifyStringResultFilter : IRouteHandlerFilter - { - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next) - { - var previousResult = await next(context); - if (previousResult is string stringResult) - { - return stringResult.ToUpperInvariant(); - } - return previousResult; - } - } } internal static class TestExtensionResults diff --git a/src/Http/Routing/src/Builder/DelegateRouteHandlerFilter.cs b/src/Http/Routing/src/Builder/DelegateRouteHandlerFilter.cs deleted file mode 100644 index 155a9b5e4b40..000000000000 --- a/src/Http/Routing/src/Builder/DelegateRouteHandlerFilter.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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; - -internal sealed class DelegateRouteHandlerFilter : IRouteHandlerFilter -{ - private readonly Func>, ValueTask> _routeHandlerFilter; - - internal DelegateRouteHandlerFilter(Func>, ValueTask> routeHandlerFilter) - { - _routeHandlerFilter = routeHandlerFilter; - } - - public ValueTask InvokeAsync(RouteHandlerFilterContext context, Func> next) - { - return _routeHandlerFilter(context, next); - } -} diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 6ce2d6c2c7ea..d8990104cb12 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -523,7 +523,7 @@ private static RouteHandlerBuilder Map( RouteParameterNames = routeParams, ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, DisableInferBodyFromParameters = disableInferBodyFromParameters, - RouteHandlerFilters = routeHandlerBuilder.RouteHandlerFilters + RouteHandlerFilterFactories = routeHandlerBuilder.RouteHandlerFilterFactories }; var filteredRequestDelegateResult = RequestDelegateFactory.Create(handler, options); // Add request delegate metadata diff --git a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs index b42e22cc3d8d..f9667538c139 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs @@ -1,7 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http; +using System.Reflection; +using Microsoft.AspNetCore.Http.Abstractions; namespace Microsoft.AspNetCore.Builder; @@ -13,7 +14,7 @@ public sealed class RouteHandlerBuilder : IEndpointConventionBuilder private readonly IEnumerable? _endpointConventionBuilders; private readonly IEndpointConventionBuilder? _endpointConventionBuilder; - internal List RouteHandlerFilters { get; } = new(); + internal List> RouteHandlerFilterFactories { get; } = new(); /// /// Instantiates a new given a single diff --git a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs index ffec088f3e73..27425caad670 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Reflection; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Http; @@ -19,7 +22,7 @@ public static class RouteHandlerFilterExtensions /// A that can be used to further customize the route handler. public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IRouteHandlerFilter filter) { - builder.RouteHandlerFilters.Add(filter); + builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => filter.InvokeAsync(context, next)); return builder; } @@ -29,9 +32,15 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// The type of the to register. /// The . /// A that can be used to further customize the route handler. - public static RouteHandlerBuilder AddFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFilterType>(this RouteHandlerBuilder builder) where TFilterType : IRouteHandlerFilter, new() + public static RouteHandlerBuilder AddFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFilterType>(this RouteHandlerBuilder builder) where TFilterType : IRouteHandlerFilter { - builder.RouteHandlerFilters.Add(new TFilterType()); + builder.RouteHandlerFilterFactories.Add((methodInfo, next) => async (context) => + { + var type = typeof(TFilterType); + var filterFactory = ActivatorUtilities.CreateFactory(type, Array.Empty()); + IRouteHandlerFilter filter = (IRouteHandlerFilter)filterFactory.Invoke(context.ServiceProvider, Array.Empty()); + return await filter.InvokeAsync(context, next); + }); return builder; } @@ -41,9 +50,21 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// The . /// A representing the core logic of the filter. /// A that can be used to further customize the route handler. - public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func>, ValueTask> routeHandlerFilter) + public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func> routeHandlerFilter) { - builder.RouteHandlerFilters.Add(new DelegateRouteHandlerFilter(routeHandlerFilter)); + builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => routeHandlerFilter(context, next)); + return builder; + } + + /// + /// Register a filter given a delegate representing the filter factory. + /// + /// The . + /// A representing the logic for constructing the filter. + /// A that can be used to further customize the route handler. + public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func filterFactory) + { + builder.RouteHandlerFilterFactories.Add(filterFactory); return builder; } } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 4cf74e9056fd..f8f646142596 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -8,5 +8,6 @@ override Microsoft.AspNetCore.Routing.RouteValuesAddress.ToString() -> string? *REMOVED*~Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, Microsoft.AspNetCore.Http.IRouteHandlerFilter! filter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! -static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func>!, System.Threading.Tasks.ValueTask>! routeHandlerFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! +static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func>! routeHandlerFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! +static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func! filterFactory) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index 1c3450159801..9d950d523dd7 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -4,15 +4,18 @@ #nullable enable using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Builder; -public class RouteHandlerEndpointRouteBuilderExtensionsTest +public class RouteHandlerEndpointRouteBuilderExtensionsTest : LoggedTest { private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) { @@ -847,6 +850,138 @@ public async Task MapMethod_DefaultsToNotThrowOnBadHttpRequestIfItCannotResolveR Assert.Equal(400, httpContext.Response.StatusCode); } + public static object[][] AddFiltersByClassData = +{ + new object[] { (Action)((RouteHandlerBuilder builder) => builder.AddFilter(new IncrementArgFilter())) }, + new object[] { (Action)((RouteHandlerBuilder builder) => builder.AddFilter()) } + }; + + [Theory] + [MemberData(nameof(AddFiltersByClassData))] + public async Task AddFilterMethods_CanRegisterFilterWithClassImplementation(Action addFilter) + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); + + string PrintId(int id) => $"ID: {id}"; + var routeHandlerBuilder = builder.Map("/{id}", PrintId); + addFilter(routeHandlerBuilder); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["id"] = "2"; + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + await endpoint.RequestDelegate!(httpContext); + + // Assert; + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal("ID: 3", body); + } + + public static object[][] AddFiltersByDelegateData = +{ + new object[] { (Action)((RouteHandlerBuilder builder) => builder.AddFilter(async (context, next) => { + context.Parameters[0] = ((int)context.Parameters[0]!) + 1; + return await next(context); + })) }, + new object[] { (Action)((RouteHandlerBuilder builder) => builder.AddFilter((methodInfo, next) => async (context) => { + context.Parameters[0] = ((int)context.Parameters[0]!) + 1; + return await next(context); + })) }, + }; + + [Theory] + [MemberData(nameof(AddFiltersByDelegateData))] + public async Task AddFilterMethods_CanRegisterFilterWithDelegateImplementation(Action addFilter) + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); + + string PrintId(int id) => $"ID: {id}"; + var routeHandlerBuilder = builder.Map("/{id}", PrintId); + addFilter(routeHandlerBuilder); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["id"] = "2"; + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + await endpoint.RequestDelegate!(httpContext); + + // Assert; + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal("ID: 3", body); + } + + [Fact] + public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatAccessesServices() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); + + string? PrintLogger(HttpContext context) => context.Items["loggerErrorIsEnabled"]?.ToString(); + var routeHandlerBuilder = builder.Map("/", PrintLogger); + routeHandlerBuilder.AddFilter(); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var httpContext = new DefaultHttpContext(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + await endpoint.RequestDelegate!(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal("True", body); + } + + class ServiceAccessingRouteHandlerFilter : IRouteHandlerFilter + { + private ILogger _logger; + + public ServiceAccessingRouteHandlerFilter(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public async ValueTask InvokeAsync(RouteHandlerFilterContext context, RouteHandlerFilterDelegate next) + { + context.HttpContext.Items["loggerErrorIsEnabled"] = _logger.IsEnabled(LogLevel.Error); + return await next(context); + } + } + + class IncrementArgFilter : IRouteHandlerFilter + { + public async ValueTask InvokeAsync(RouteHandlerFilterContext context, RouteHandlerFilterDelegate next) + { + context.Parameters[0] = ((int)context.Parameters[0]!) + 1; + return await next(context); + } + } + class FromRoute : Attribute, IFromRouteMetadata { public string? Name { get; set; } From 39b87214ab9c1c05380f82cad9b9509355f19a6b Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 11 Mar 2022 12:10:00 -0800 Subject: [PATCH 02/10] Address feedback from peer review --- .../Http.Abstractions/src/IRouteHandlerFilter.cs | 2 -- .../Http.Abstractions/src/PublicAPI.Unshipped.txt | 7 +++---- .../src/RouteHandlerFilterContext.cs | 9 +-------- .../src/RouteHandlerFilterDelegate.cs | 2 +- .../Http.Extensions/src/PublicAPI.Unshipped.txt | 2 +- .../Http.Extensions/src/RequestDelegateFactory.cs | 10 +++++----- .../src/RequestDelegateFactoryOptions.cs | 1 - .../test/RequestDelegateFactoryTests.cs | 1 - src/Http/Routing/src/Builder/RouteHandlerBuilder.cs | 2 +- .../src/Builder/RouteHandlerFilterExtensions.cs | 13 +++++++------ src/Http/Routing/src/PublicAPI.Unshipped.txt | 4 ++-- ...outeHandlerEndpointRouteBuilderExtensionsTest.cs | 1 - 12 files changed, 21 insertions(+), 33 deletions(-) diff --git a/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs b/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs index 0897394b95e9..e271d4bcdfff 100644 --- a/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs +++ b/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http.Abstractions; - namespace Microsoft.AspNetCore.Http; /// diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index e7724abff1e8..1b52b13b0bf3 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,12 +1,11 @@ #nullable enable *REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string! -Microsoft.AspNetCore.Http.Abstractions.RouteHandlerFilterDelegate Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T! -Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerFilterContext! context, Microsoft.AspNetCore.Http.Abstractions.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerFilterContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? -Microsoft.AspNetCore.Http.RouteHandlerFilterContext.RouteHandlerFilterContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.IServiceProvider! serviceProvider, params object![]! parameters) -> void -Microsoft.AspNetCore.Http.RouteHandlerFilterContext.ServiceProvider.get -> System.IServiceProvider! +Microsoft.AspNetCore.Http.RouteHandlerFilterContext.RouteHandlerFilterContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! parameters) -> void +Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(Microsoft.AspNetCore.Routing.RouteValueDictionary? dictionary) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable>? values) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable>? values) -> void diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs b/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs index 0c42b5a7179e..5d588df9ccfa 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs @@ -13,13 +13,11 @@ public class RouteHandlerFilterContext /// Creates a new instance of the for a given request. /// /// The associated with the current request. - /// The associated with the current request. /// A list of parameters provided in the current request. - public RouteHandlerFilterContext(HttpContext httpContext, IServiceProvider serviceProvider, params object[] parameters) + public RouteHandlerFilterContext(HttpContext httpContext, params object[] parameters) { HttpContext = httpContext; Parameters = parameters; - ServiceProvider = serviceProvider; } /// @@ -34,9 +32,4 @@ public RouteHandlerFilterContext(HttpContext httpContext, IServiceProvider servi /// /// public IList Parameters { get; } - - /// - /// The associated with the current request. - /// - public IServiceProvider ServiceProvider { get; } } diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs index a3fd423f6e3e..ce873587a532 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs @@ -1,7 +1,7 @@ // 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.Abstractions; +namespace Microsoft.AspNetCore.Http; /// /// A delegate that is applied as a filter on a route handler. diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index faeb31077dbf..2076fe515a66 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? 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! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index deb502f9a40d..d3bd3e71f884 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -10,7 +10,6 @@ using System.Security.Claims; using System.Text; using System.Text.Json; -using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -80,7 +79,7 @@ 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(IServiceProvider), typeof(object[]) })!; + 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))!); @@ -206,7 +205,7 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions Expression.Assign( InvokedFilterContextExpr, Expression.New(RouteHandlerFilterContextConstructor, - new Expression[] { HttpContextExpr, RequestServicesExpr, Expression.NewArrayInit(typeof(object), factoryContext.BoxedArgs) })), + new Expression[] { HttpContextExpr, Expression.NewArrayInit(typeof(object), factoryContext.BoxedArgs) })), Expression.Invoke(invokePipeline, InvokedFilterContextExpr) ); } @@ -244,9 +243,10 @@ target is null for (var i = factoryContext.Filters.Count - 1; i >= 0; i--) { - var currentFilterFactory = factoryContext.Filters![i]; + var currentFilterFactory = factoryContext.Filters[i]; var nextFilter = filteredInvocation; - filteredInvocation = (RouteHandlerFilterContext context) => currentFilterFactory(methodInfo, nextFilter)(context); + var currentFilter = currentFilterFactory(methodInfo, nextFilter); + filteredInvocation = (RouteHandlerFilterContext context) => currentFilter(context); } return filteredInvocation; diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 19e431eee103..93cedd139df7 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; -using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Logging; diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index b7fc626b3806..d39af64f36ac 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -19,7 +19,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.Metadata; diff --git a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs index f9667538c139..a913c3060a4d 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; -using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder; diff --git a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs index 27425caad670..2efa68ccf714 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Http; @@ -34,12 +33,14 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// A that can be used to further customize the route handler. public static RouteHandlerBuilder AddFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFilterType>(this RouteHandlerBuilder builder) where TFilterType : IRouteHandlerFilter { - builder.RouteHandlerFilterFactories.Add((methodInfo, next) => async (context) => + var type = typeof(TFilterType); + // We can provide the `MethodInfo` as an argument to the factory here so that `IRouteHandlerFilter` + // implementors can access the MethodInfo in their constructors. + var filterFactory = ActivatorUtilities.CreateFactory(type, Array.Empty()); + builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => { - var type = typeof(TFilterType); - var filterFactory = ActivatorUtilities.CreateFactory(type, Array.Empty()); - IRouteHandlerFilter filter = (IRouteHandlerFilter)filterFactory.Invoke(context.ServiceProvider, Array.Empty()); - return await filter.InvokeAsync(context, next); + IRouteHandlerFilter filter = (IRouteHandlerFilter)filterFactory.Invoke(context.HttpContext.RequestServices, Array.Empty()); + return filter.InvokeAsync(context, next); }); return builder; } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index f8f646142596..a546206d05ab 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -8,6 +8,6 @@ override Microsoft.AspNetCore.Routing.RouteValuesAddress.ToString() -> string? *REMOVED*~Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, Microsoft.AspNetCore.Http.IRouteHandlerFilter! filter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! -static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func>! routeHandlerFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! -static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func! filterFactory) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! +static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func>! routeHandlerFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! +static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func! filterFactory) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index 9d950d523dd7..ce7d101ae023 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -4,7 +4,6 @@ #nullable enable using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; From ddd5b59c0ad911e3db2b5866c11a970f645b6be1 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 11 Mar 2022 19:28:53 -0800 Subject: [PATCH 03/10] Apply suggestions from code review Co-authored-by: David Fowler --- src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs | 2 +- src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs index ce873587a532..9a04d524e0d3 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs @@ -8,6 +8,6 @@ namespace Microsoft.AspNetCore.Http; /// /// The associated with the current request. /// -/// An awaitable result of calling the handler and applying any modifications made by filters in the pipeline. +/// A result of calling the handler and applying any modifications made by filters in the pipeline. /// public delegate ValueTask RouteHandlerFilterDelegate(RouteHandlerFilterContext context); diff --git a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs index 2efa68ccf714..707392826723 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs @@ -33,13 +33,12 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// A that can be used to further customize the route handler. public static RouteHandlerBuilder AddFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFilterType>(this RouteHandlerBuilder builder) where TFilterType : IRouteHandlerFilter { - var type = typeof(TFilterType); // We can provide the `MethodInfo` as an argument to the factory here so that `IRouteHandlerFilter` // implementors can access the MethodInfo in their constructors. - var filterFactory = ActivatorUtilities.CreateFactory(type, Array.Empty()); + var filterFactory = ActivatorUtilities.CreateFactory(typeof(TFilterType), Array.Empty()); builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => { - IRouteHandlerFilter filter = (IRouteHandlerFilter)filterFactory.Invoke(context.HttpContext.RequestServices, Array.Empty()); + var filter = (IRouteHandlerFilter)filterFactory.Invoke(context.HttpContext.RequestServices, Array.Empty()); return filter.InvokeAsync(context, next); }); return builder; From 4b39ed4198d5f88e94cf56468a1ccee96d54885a Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 14 Mar 2022 09:10:04 -0700 Subject: [PATCH 04/10] Fix XML comment for RouteHandlerFilterDelegate --- src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs index 9a04d524e0d3..471f62789c52 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs @@ -8,6 +8,6 @@ namespace Microsoft.AspNetCore.Http; /// /// The associated with the current request. /// -/// A result of calling the handler and applying any modifications made by filters in the pipeline. +/// A result of calling the handler and applying any modifications made by filters in the pipeline. /// public delegate ValueTask RouteHandlerFilterDelegate(RouteHandlerFilterContext context); From fb6fefde462d6d710594bc6b5bad4139f919a5a5 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 14 Mar 2022 14:59:10 -0700 Subject: [PATCH 05/10] React to feedback from API review --- .../src/IRouteHandlerFilter.cs | 6 ++-- .../src/PublicAPI.Unshipped.txt | 14 +++++--- .../src/RouteHandlerContext.cs | 34 +++++++++++++++++++ .../src/RouteHandlerFilterDelegate.cs | 4 +-- ...xt.cs => RouteHandlerInvocationContext.cs} | 6 ++-- .../src/PublicAPI.Unshipped.txt | 2 +- .../src/RequestDelegateFactory.cs | 28 ++++++++------- .../src/RequestDelegateFactoryOptions.cs | 3 +- .../test/RequestDelegateFactoryTests.cs | 34 +++++++++---------- .../src/Builder/RouteHandlerBuilder.cs | 3 +- .../Builder/RouteHandlerFilterExtensions.cs | 5 ++- src/Http/Routing/src/PublicAPI.Unshipped.txt | 4 +-- ...ndlerEndpointRouteBuilderExtensionsTest.cs | 4 +-- 13 files changed, 93 insertions(+), 54 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/RouteHandlerContext.cs rename src/Http/Http.Abstractions/src/{RouteHandlerFilterContext.cs => RouteHandlerInvocationContext.cs} (82%) diff --git a/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs b/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs index e271d4bcdfff..854947c4b351 100644 --- a/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs +++ b/src/Http/Http.Abstractions/src/IRouteHandlerFilter.cs @@ -9,12 +9,12 @@ namespace Microsoft.AspNetCore.Http; public interface IRouteHandlerFilter { /// - /// Implements the core logic associated with the filter given a + /// Implements the core logic associated with the filter given a /// and the next filter to call in the pipeline. /// - /// The associated with the current request/response. + /// The associated with the current request/response. /// The next filter in the pipeline. /// An awaitable result of calling the handler and apply /// any modifications made by filters in the pipeline. - ValueTask InvokeAsync(RouteHandlerFilterContext context, RouteHandlerFilterDelegate next); + ValueTask InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next); } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 1b52b13b0bf3..4b84285e9979 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,17 +1,21 @@ #nullable enable *REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string! Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T! -Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerFilterContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? -Microsoft.AspNetCore.Http.RouteHandlerFilterContext.RouteHandlerFilterContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! parameters) -> void +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! +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>? values) -> void Microsoft.AspNetCore.Routing.RouteValueDictionary.RouteValueDictionary(System.Collections.Generic.IEnumerable>? 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! Microsoft.AspNetCore.Http.IRouteHandlerFilter diff --git a/src/Http/Http.Abstractions/src/RouteHandlerContext.cs b/src/Http/Http.Abstractions/src/RouteHandlerContext.cs new file mode 100644 index 000000000000..29b9333c6a5e --- /dev/null +++ b/src/Http/Http.Abstractions/src/RouteHandlerContext.cs @@ -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; + +/// +/// Represents the information accessible via the route handler filter +/// API when the user is constructing a new route handler. +/// +public class RouteHandlerContext +{ + /// + /// Creates a new instance of the . + /// + /// The associated with the route handler of the current request. + /// The associated with the endpoint the filter is targeting. + public RouteHandlerContext(MethodInfo methodInfo, EndpointMetadataCollection endpointMetadata) + { + MethodInfo = methodInfo; + EndpointMetadata = endpointMetadata; + } + + /// + /// The associated with the current route handler. + /// + public MethodInfo MethodInfo { get; } + + /// + /// The associated with the current endpoint. + /// + public EndpointMetadataCollection EndpointMetadata{ get; } +} diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs index 471f62789c52..afc443a33a7d 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerFilterDelegate.cs @@ -6,8 +6,8 @@ namespace Microsoft.AspNetCore.Http; /// /// A delegate that is applied as a filter on a route handler. /// -/// The associated with the current request. +/// The associated with the current request. /// /// A result of calling the handler and applying any modifications made by filters in the pipeline. /// -public delegate ValueTask RouteHandlerFilterDelegate(RouteHandlerFilterContext context); +public delegate ValueTask RouteHandlerFilterDelegate(RouteHandlerInvocationContext context); diff --git a/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs b/src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs similarity index 82% rename from src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs rename to src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs index 5d588df9ccfa..6980bd9a5ea4 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerFilterContext.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs @@ -7,14 +7,14 @@ namespace Microsoft.AspNetCore.Http; /// Provides an abstraction for wrapping the and parameters /// provided to a route handler. /// -public class RouteHandlerFilterContext +public class RouteHandlerInvocationContext { /// - /// Creates a new instance of the for a given request. + /// Creates a new instance of the for a given request. /// /// The associated with the current request. /// A list of parameters provided in the current request. - public RouteHandlerFilterContext(HttpContext httpContext, params object[] parameters) + public RouteHandlerInvocationContext(HttpContext httpContext, params object[] parameters) { HttpContext = httpContext; Parameters = parameters; diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 2076fe515a66..0420ef06a9cf 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? 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! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index d3bd3e71f884..856e17ab471c 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -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" }; @@ -196,15 +196,15 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions if (factoryContext.Filters is { Count: > 0 }) { var filterPipeline = CreateFilterPipeline(methodInfo, targetExpression, factoryContext); - Expression>> invokePipeline = (context) => filterPipeline(context); + Expression>> invokePipeline = (context) => filterPipeline(context); returnType = typeof(ValueTask); - // 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) ); @@ -245,8 +245,12 @@ target is null { var currentFilterFactory = factoryContext.Filters[i]; var nextFilter = filteredInvocation; - var currentFilter = currentFilterFactory(methodInfo, nextFilter); - filteredInvocation = (RouteHandlerFilterContext context) => currentFilter(context); + var currentFilter = currentFilterFactory( + new RouteHandlerContext( + methodInfo, + new EndpointMetadataCollection(factoryContext.Metadata)), + nextFilter); + filteredInvocation = (RouteHandlerInvocationContext context) => currentFilter(context); } return filteredInvocation; @@ -265,7 +269,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( @@ -1694,7 +1698,7 @@ private class FactoryContext public List ContextArgAccess { get; } = new(); public Expression? MethodCall { get; set; } public List BoxedArgs { get; } = new(); - public List>? Filters { get; init; } + public List>? Filters { get; init; } } private static class RequestDelegateFactoryConstants diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 93cedd139df7..70207f9c63d8 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -1,7 +1,6 @@ // 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; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Logging; @@ -36,5 +35,5 @@ public sealed class RequestDelegateFactoryOptions /// /// The list of filters that must run in the pipeline for a given route handler. /// - public IReadOnlyList>? RouteHandlerFilterFactories { get; init; } + public IReadOnlyList>? RouteHandlerFilterFactories { get; init; } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index d39af64f36ac..f7eb78acd909 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4216,9 +4216,9 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilterFactories = new List>() + RouteHandlerFilterFactories = new List>() { - (methodInfo, next) => async (context) => + (routeHandlerContext, next) => async (context) => { context.Parameters[0] = context.Parameters[0] != null ? $"{((string)context.Parameters[0]!)}Prefix" : "NULL"; return await next(context); @@ -4250,8 +4250,8 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilterFactories = new List>() { - (methodInfo, next) => async (context) => + RouteHandlerFilterFactories = new List>() { + (routeHandlerContext, next) => async (context) => { if (context.HttpContext.Response.StatusCode == 400) { @@ -4296,14 +4296,14 @@ string HelloName(string name, int age) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilterFactories = new List>() + RouteHandlerFilterFactories = new List>() { - (methodInfo, next) => async (context) => + (routeHandlerContext, next) => async (context) => { context.Parameters[1] = ((int)context.Parameters[1]!) + 2; return await next(context); }, - (methodInfo, next) => async (context) => + (routeHandlerContext, next) => async (context) => { foreach (var parameter in context.Parameters) { @@ -4344,11 +4344,11 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilterFactories = new List>() + RouteHandlerFilterFactories = new List>() { - (methodInfo, next) => + (routeHandlerContext, next) => { - var parameters = methodInfo.GetParameters(); + var parameters = routeHandlerContext.MethodInfo.GetParameters(); var isInt = parameters.Length == 2 && parameters[1].ParameterType == typeof(int); return async (context) => { @@ -4395,9 +4395,9 @@ string PrintTodo(Todo todo) // Act var factoryResult = RequestDelegateFactory.Create(PrintTodo, new RequestDelegateFactoryOptions() { - RouteHandlerFilterFactories = new List>() + RouteHandlerFilterFactories = new List>() { - (methodInfo, next) => async (context) => + (routeHandlerContext, next) => async (context) => { Todo originalTodo = (Todo)context.Parameters[0]!; originalTodo!.IsComplete = !originalTodo.IsComplete; @@ -4436,9 +4436,9 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilterFactories = new List>() + RouteHandlerFilterFactories = new List>() { - (methodInfo, next) => async (context) => + (routeHandlerContext, next) => async (context) => { var previousResult = await next(context); if (previousResult is string stringResult) @@ -4479,9 +4479,9 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - RouteHandlerFilterFactories = new List>() + RouteHandlerFilterFactories = new List>() { - (methodInfo, next) => async (context) => + (routeHandlerContext, next) => async (context) => { var previousResult = await next(context); if (previousResult is string stringResult) @@ -4490,7 +4490,7 @@ string HelloName(string name) } return previousResult; }, - (methodInfo, next) => async (context) => + (RouteHandlerContext, next) => async (context) => { context.Parameters[0] = context.Parameters[0] != null ? $"{((string)context.Parameters[0]!)}Prefix" : "NULL"; return await next(context); diff --git a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs index a913c3060a4d..735178ff4214 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs @@ -1,7 +1,6 @@ // 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; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder; @@ -14,7 +13,7 @@ public sealed class RouteHandlerBuilder : IEndpointConventionBuilder private readonly IEnumerable? _endpointConventionBuilders; private readonly IEndpointConventionBuilder? _endpointConventionBuilder; - internal List> RouteHandlerFilterFactories { get; } = new(); + internal List> RouteHandlerFilterFactories { get; } = new(); /// /// Instantiates a new given a single diff --git a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs index 707392826723..cff9f8db9461 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -50,7 +49,7 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// The . /// A representing the core logic of the filter. /// A that can be used to further customize the route handler. - public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func> routeHandlerFilter) + public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func> routeHandlerFilter) { builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => routeHandlerFilter(context, next)); return builder; @@ -62,7 +61,7 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Fu /// The . /// A representing the logic for constructing the filter. /// A that can be used to further customize the route handler. - public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func filterFactory) + public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func filterFactory) { builder.RouteHandlerFilterFactories.Add(filterFactory); return builder; diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index a546206d05ab..ce55a2d93bb8 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -8,6 +8,6 @@ override Microsoft.AspNetCore.Routing.RouteValuesAddress.ToString() -> string? *REMOVED*~Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, Microsoft.AspNetCore.Http.IRouteHandlerFilter! filter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! -static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func>! routeHandlerFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! -static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func! filterFactory) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! +static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func! filterFactory) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! +static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func>! routeHandlerFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder! diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index ce7d101ae023..d2f798b62f1d 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -965,7 +965,7 @@ public ServiceAccessingRouteHandlerFilter(ILoggerFactory loggerFactory) _logger = loggerFactory.CreateLogger(); } - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, RouteHandlerFilterDelegate next) + public async ValueTask InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next) { context.HttpContext.Items["loggerErrorIsEnabled"] = _logger.IsEnabled(LogLevel.Error); return await next(context); @@ -974,7 +974,7 @@ public ServiceAccessingRouteHandlerFilter(ILoggerFactory loggerFactory) class IncrementArgFilter : IRouteHandlerFilter { - public async ValueTask InvokeAsync(RouteHandlerFilterContext context, RouteHandlerFilterDelegate next) + public async ValueTask InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next) { context.Parameters[0] = ((int)context.Parameters[0]!) + 1; return await next(context); From e8032e214eb825237dc0e55f096109e715fe3d9c Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 14 Mar 2022 15:23:44 -0700 Subject: [PATCH 06/10] Fix up RouteHandlerContext instantiation --- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 856e17ab471c..c32a6a790147 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -240,16 +240,15 @@ 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 currentFilterFactory = factoryContext.Filters[i]; var nextFilter = filteredInvocation; - var currentFilter = currentFilterFactory( - new RouteHandlerContext( - methodInfo, - new EndpointMetadataCollection(factoryContext.Metadata)), - nextFilter); + var currentFilter = currentFilterFactory(routeHandlerContext, nextFilter); filteredInvocation = (RouteHandlerInvocationContext context) => currentFilter(context); } From c600fc4fbc8c501d5bf22ae5c4e09a33a662e010 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 14 Mar 2022 22:23:01 -0700 Subject: [PATCH 07/10] Address more feedback from peer review --- .../src/RouteHandlerContext.cs | 4 +- .../src/RouteHandlerInvocationContext.cs | 2 +- .../test/RequestDelegateFactoryTests.cs | 55 +++++++++++++++++++ .../Builder/RouteHandlerFilterExtensions.cs | 8 +-- ...ndlerEndpointRouteBuilderExtensionsTest.cs | 9 ++- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/Http/Http.Abstractions/src/RouteHandlerContext.cs b/src/Http/Http.Abstractions/src/RouteHandlerContext.cs index 29b9333c6a5e..fdf54a3b9bb5 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerContext.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerContext.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Http; /// Represents the information accessible via the route handler filter /// API when the user is constructing a new route handler. /// -public class RouteHandlerContext +public sealed class RouteHandlerContext { /// /// Creates a new instance of the . @@ -30,5 +30,5 @@ public RouteHandlerContext(MethodInfo methodInfo, EndpointMetadataCollection end /// /// The associated with the current endpoint. /// - public EndpointMetadataCollection EndpointMetadata{ get; } + public EndpointMetadataCollection EndpointMetadata { get; } } diff --git a/src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs b/src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs index 6980bd9a5ea4..d7cfe600a760 100644 --- a/src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs +++ b/src/Http/Http.Abstractions/src/RouteHandlerInvocationContext.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Http; /// Provides an abstraction for wrapping the and parameters /// provided to a route handler. /// -public class RouteHandlerInvocationContext +public sealed class RouteHandlerInvocationContext { /// /// Creates a new instance of the for a given request. diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index f7eb78acd909..f597d15edc15 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4370,6 +4370,61 @@ string HelloName(string name) Assert.Equal("Is not an int.", responseBody); } + [Fact] + public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatUsesEndpointMetadata() + { + // Arrange + string HelloName(IFormFileCollection formFiles) + { + return $"Got {formFiles.Count} files."; + }; + + var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream"); + var form = new MultipartFormDataContent("some-boundary"); + form.Add(fileContent, "file", "file.txt"); + + var stream = new MemoryStream(); + await form.CopyToAsync(stream); + + stream.Seek(0, SeekOrigin.Begin); + + var httpContext = CreateHttpContext(); + httpContext.Request.Body = stream; + httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary"; + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + // Act + var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() + { + RouteHandlerFilterFactories = new List>() + { + (routeHandlerContext, next) => + { + var acceptsMetadata = routeHandlerContext.EndpointMetadata.OfType(); + var contentType = acceptsMetadata.SingleOrDefault()?.ContentTypes.SingleOrDefault(); + + return async (context) => + { + if (contentType == "multipart/form-data") + { + return "I see you expect a form."; + } + return await next(context); + }; + }, + } + }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); + + // Assert + var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal("I see you expect a form.", responseBody); + } + [Fact] public async Task RequestDelegateFactory_CanInvokeSingleEndpointFilter_ThatModifiesBodyParameter() { diff --git a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs index cff9f8db9461..bc1a58013e6f 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs @@ -32,12 +32,10 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// A that can be used to further customize the route handler. public static RouteHandlerBuilder AddFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFilterType>(this RouteHandlerBuilder builder) where TFilterType : IRouteHandlerFilter { - // We can provide the `MethodInfo` as an argument to the factory here so that `IRouteHandlerFilter` - // implementors can access the MethodInfo in their constructors. - var filterFactory = ActivatorUtilities.CreateFactory(typeof(TFilterType), Array.Empty()); - builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => + var filterFactory = ActivatorUtilities.CreateFactory(typeof(TFilterType), new[] { typeof(RouteHandlerContext) }); + builder.RouteHandlerFilterFactories.Add((routeHandlerContext, next) => (context) => { - var filter = (IRouteHandlerFilter)filterFactory.Invoke(context.HttpContext.RequestServices, Array.Empty()); + var filter = (IRouteHandlerFilter)filterFactory.Invoke(context.HttpContext.RequestServices, new [] { routeHandlerContext }); return filter.InvokeAsync(context, next); }); return builder; diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index d2f798b62f1d..81eb3cd64f50 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -932,7 +932,7 @@ public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatAccessesSer { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); - string? PrintLogger(HttpContext context) => context.Items["loggerErrorIsEnabled"]?.ToString(); + string? PrintLogger(HttpContext context) => $"loggerErrorIsEnabled: {context.Items["loggerErrorIsEnabled"]}, parentName: {context.Items["parentName"]}"; var routeHandlerBuilder = builder.Map("/", PrintLogger); routeHandlerBuilder.AddFilter(); @@ -953,21 +953,24 @@ public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatAccessesSer httpResponse.Body.Seek(0, SeekOrigin.Begin); var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; - Assert.Equal("True", body); + Assert.Equal("loggerErrorIsEnabled: True, parentName: RouteHandlerEndpointRouteBuilderExtensionsTest", body); } class ServiceAccessingRouteHandlerFilter : IRouteHandlerFilter { private ILogger _logger; + private RouteHandlerContext _routeHandlerContext; - public ServiceAccessingRouteHandlerFilter(ILoggerFactory loggerFactory) + public ServiceAccessingRouteHandlerFilter(ILoggerFactory loggerFactory, RouteHandlerContext context) { _logger = loggerFactory.CreateLogger(); + _routeHandlerContext = context; } public async ValueTask InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next) { context.HttpContext.Items["loggerErrorIsEnabled"] = _logger.IsEnabled(LogLevel.Error); + context.HttpContext.Items["parentName"] = _routeHandlerContext.MethodInfo.DeclaringType?.Name; return await next(context); } } From a55381fe3653f068acaa00e4b50c49015b83f157 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 15 Mar 2022 10:31:53 -0700 Subject: [PATCH 08/10] Remove RouteHandlerContext passing --- .../Routing/src/Builder/RouteHandlerFilterExtensions.cs | 4 ++-- .../RouteHandlerEndpointRouteBuilderExtensionsTest.cs | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs index bc1a58013e6f..f1927c01a43e 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs @@ -32,10 +32,10 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// A that can be used to further customize the route handler. public static RouteHandlerBuilder AddFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFilterType>(this RouteHandlerBuilder builder) where TFilterType : IRouteHandlerFilter { - var filterFactory = ActivatorUtilities.CreateFactory(typeof(TFilterType), new[] { typeof(RouteHandlerContext) }); + var filterFactory = ActivatorUtilities.CreateFactory(typeof(TFilterType), Type.EmptyTypes); builder.RouteHandlerFilterFactories.Add((routeHandlerContext, next) => (context) => { - var filter = (IRouteHandlerFilter)filterFactory.Invoke(context.HttpContext.RequestServices, new [] { routeHandlerContext }); + var filter = (IRouteHandlerFilter)filterFactory.Invoke(context.HttpContext.RequestServices, Array.Empty()); return filter.InvokeAsync(context, next); }); return builder; diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index 81eb3cd64f50..e98e6ecce1ed 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -932,7 +932,7 @@ public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatAccessesSer { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); - string? PrintLogger(HttpContext context) => $"loggerErrorIsEnabled: {context.Items["loggerErrorIsEnabled"]}, parentName: {context.Items["parentName"]}"; + string? PrintLogger(HttpContext context) => $"loggerErrorIsEnabled: {context.Items["loggerErrorIsEnabled"]}"; var routeHandlerBuilder = builder.Map("/", PrintLogger); routeHandlerBuilder.AddFilter(); @@ -953,24 +953,21 @@ public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatAccessesSer httpResponse.Body.Seek(0, SeekOrigin.Begin); var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; - Assert.Equal("loggerErrorIsEnabled: True, parentName: RouteHandlerEndpointRouteBuilderExtensionsTest", body); + Assert.Equal("loggerErrorIsEnabled: True", body); } class ServiceAccessingRouteHandlerFilter : IRouteHandlerFilter { private ILogger _logger; - private RouteHandlerContext _routeHandlerContext; - public ServiceAccessingRouteHandlerFilter(ILoggerFactory loggerFactory, RouteHandlerContext context) + public ServiceAccessingRouteHandlerFilter(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); - _routeHandlerContext = context; } public async ValueTask InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next) { context.HttpContext.Items["loggerErrorIsEnabled"] = _logger.IsEnabled(LogLevel.Error); - context.HttpContext.Items["parentName"] = _routeHandlerContext.MethodInfo.DeclaringType?.Name; return await next(context); } } From 87adb2adc3cb5c856c5053fa7d7ca1f4b13aa9e2 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 15 Mar 2022 14:21:47 -0700 Subject: [PATCH 09/10] Remove old RouteHandlerFilterFactories implementation --- src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 6b15cd89295b..f5825c4e8476 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -3,8 +3,6 @@ Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactor 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! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilters.get -> System.Collections.Generic.IReadOnlyList? -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! From a2c24523db19a94286e3aa3e05060161de45dc6a Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 15 Mar 2022 17:14:51 -0700 Subject: [PATCH 10/10] Fix up RouteHandlerContext references --- .../Builder/RouteHandlerFilterExtensions.cs | 4 +- ...ndlerEndpointRouteBuilderExtensionsTest.cs | 38 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs index f1927c01a43e..fae8885ad24a 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerFilterExtensions.cs @@ -20,7 +20,7 @@ public static class RouteHandlerFilterExtensions /// A that can be used to further customize the route handler. public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IRouteHandlerFilter filter) { - builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => filter.InvokeAsync(context, next)); + builder.RouteHandlerFilterFactories.Add((routeHandlerContext, next) => (context) => filter.InvokeAsync(context, next)); return builder; } @@ -49,7 +49,7 @@ public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, IR /// A that can be used to further customize the route handler. public static RouteHandlerBuilder AddFilter(this RouteHandlerBuilder builder, Func> routeHandlerFilter) { - builder.RouteHandlerFilterFactories.Add((methodInfo, next) => (context) => routeHandlerFilter(context, next)); + builder.RouteHandlerFilterFactories.Add((routeHandlerContext, next) => (context) => routeHandlerFilter(context, next)); return builder; } diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index e98e6ecce1ed..b9d4e586076f 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -885,17 +885,33 @@ public async Task AddFilterMethods_CanRegisterFilterWithClassImplementation(Acti Assert.Equal("ID: 3", body); } - public static object[][] AddFiltersByDelegateData = -{ - new object[] { (Action)((RouteHandlerBuilder builder) => builder.AddFilter(async (context, next) => { - context.Parameters[0] = ((int)context.Parameters[0]!) + 1; - return await next(context); - })) }, - new object[] { (Action)((RouteHandlerBuilder builder) => builder.AddFilter((methodInfo, next) => async (context) => { - context.Parameters[0] = ((int)context.Parameters[0]!) + 1; - return await next(context); - })) }, - }; + public static object[][] AddFiltersByDelegateData + { + get + { + void WithFilter(RouteHandlerBuilder builder) => + builder.AddFilter(async (context, next) => + { + context.Parameters[0] = ((int)context.Parameters[0]!) + 1; + return await next(context); + }); + + void WithFilterFactory(RouteHandlerBuilder builder) => + builder.AddFilter((routeHandlerContext, next) => async (context) => + { + Assert.NotNull(routeHandlerContext.MethodInfo); + Assert.NotNull(routeHandlerContext.MethodInfo.DeclaringType); + Assert.Equal("RouteHandlerEndpointRouteBuilderExtensionsTest", routeHandlerContext.MethodInfo.DeclaringType?.Name); + context.Parameters[0] = ((int)context.Parameters[0]!) + 1; + return await next(context); + }); + + return new object[][] { + new object[] { (Action)WithFilter }, + new object[] { (Action)WithFilterFactory } + }; + } + } [Theory] [MemberData(nameof(AddFiltersByDelegateData))]