diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 5aed25374d9c..349f4020c45a 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -157,6 +157,12 @@ Microsoft.AspNetCore.Http.Headers.ResponseHeaders.SetCookie.get -> System.Collec Microsoft.AspNetCore.Http.Headers.ResponseHeaders.SetCookie.set -> void Microsoft.AspNetCore.Http.Headers.ResponseHeaders.SetList(string! name, System.Collections.Generic.IList? values) -> void Microsoft.AspNetCore.Http.RequestDelegateFactory +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RequestDelegateFactoryOptions() -> void +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteParameterNames.get -> System.Collections.Generic.IEnumerable? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteParameterNames.init -> void +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ServiceProvider.get -> System.IServiceProvider? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ServiceProvider.init -> void Microsoft.AspNetCore.Mvc.ProblemDetails Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void @@ -186,9 +192,8 @@ static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.AppendList(th static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpRequest! request) -> Microsoft.AspNetCore.Http.Headers.RequestHeaders! static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpResponse! response) -> Microsoft.AspNetCore.Http.Headers.ResponseHeaders! static Microsoft.AspNetCore.Http.HttpContextServerVariableExtensions.GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext! context, string! variableName) -> string? -static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, System.IServiceProvider? serviceProvider) -> Microsoft.AspNetCore.Http.RequestDelegate! -static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.IServiceProvider? serviceProvider) -> Microsoft.AspNetCore.Http.RequestDelegate! -static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.IServiceProvider? serviceProvider, System.Func! targetFactory) -> Microsoft.AspNetCore.Http.RequestDelegate! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate! static Microsoft.AspNetCore.Http.ResponseExtensions.Clear(this Microsoft.AspNetCore.Http.HttpResponse! response) -> void static Microsoft.AspNetCore.Http.ResponseExtensions.Redirect(this Microsoft.AspNetCore.Http.HttpResponse! response, string! location, bool permanent, bool preserveMethod) -> void static Microsoft.AspNetCore.Http.SendFileResponseExtensions.SendFileAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, Microsoft.Extensions.FileProviders.IFileInfo! file, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 083f5318044c..c30b31a8f8ee 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -60,9 +60,11 @@ public static partial class RequestDelegateFactory /// Creates a implementation for . /// /// A request handler with any number of custom parameters that often produces a response with its return value. - /// The instance used to detect which parameters are services. + /// The used to configure the behavior of the handler. /// The . - public static RequestDelegate Create(Delegate action, IServiceProvider? serviceProvider) +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOptions? options = null) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { if (action is null) { @@ -75,7 +77,7 @@ public static RequestDelegate Create(Delegate action, IServiceProvider? serviceP null => null, }; - var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, serviceProvider, targetExpression); + var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, targetExpression); return httpContext => { @@ -83,53 +85,44 @@ public static RequestDelegate Create(Delegate action, IServiceProvider? serviceP }; } - /// - /// Creates a implementation for . - /// - /// A static request handler with any number of custom parameters that often produces a response with its return value. - /// The instance used to detect which parameters are services. - /// The . - public static RequestDelegate Create(MethodInfo methodInfo, IServiceProvider? serviceProvider) - { - if (methodInfo is null) - { - throw new ArgumentNullException(nameof(methodInfo)); - } - - var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, serviceProvider, targetExpression: null); - - return httpContext => - { - return targetableRequestDelegate(null, httpContext); - }; - } - /// /// Creates a implementation for . /// /// A request handler with any number of custom parameters that often produces a response with its return value. - /// The instance used to detect which parameters are services. /// Creates the for the non-static method. + /// The used to configure the behavior of the handler. /// The . - public static RequestDelegate Create(MethodInfo methodInfo, IServiceProvider? serviceProvider, Func targetFactory) +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static RequestDelegate Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { if (methodInfo is null) { throw new ArgumentNullException(nameof(methodInfo)); } - if (targetFactory is null) + if (methodInfo.DeclaringType is null) { - throw new ArgumentNullException(nameof(targetFactory)); + throw new ArgumentException($"{nameof(methodInfo)} does not have a declaring type."); } - if (methodInfo.DeclaringType is null) + if (targetFactory is null) { - throw new ArgumentException($"A {nameof(targetFactory)} was provided, but {nameof(methodInfo)} does not have a Declaring type."); + if (methodInfo.IsStatic) + { + var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression: null); + + return httpContext => + { + return untargetableRequestDelegate(null, httpContext); + }; + } + + targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!; } var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType); - var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, serviceProvider, targetExpression); + var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression); return httpContext => { @@ -137,7 +130,7 @@ public static RequestDelegate Create(MethodInfo methodInfo, IServiceProvider? se }; } - private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, IServiceProvider? serviceProvider, Expression? targetExpression) + private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, Expression? targetExpression) { // Non void return type @@ -157,9 +150,14 @@ public static RequestDelegate Create(MethodInfo methodInfo, IServiceProvider? se var factoryContext = new FactoryContext() { - ServiceProviderIsService = serviceProvider?.GetService() + ServiceProviderIsService = options?.ServiceProvider?.GetService() }; + if (options?.RouteParameterNames is { } routeParameterNames) + { + factoryContext.RouteParameters = new(routeParameterNames); + } + var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext); var responseWritingMethodCall = factoryContext.TryParseParams.Count > 0 ? @@ -202,6 +200,11 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext if (parameterCustomAttributes.OfType().FirstOrDefault() is { } routeAttribute) { + if (factoryContext.RouteParameters is { } routeParams && !routeParams.Contains(parameter.Name)) + { + throw new InvalidOperationException($"{parameter.Name} is not a route paramter."); + } + return BindParameterFromProperty(parameter, RouteValuesExpr, routeAttribute.Name ?? parameter.Name, factoryContext); } else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } queryAttribute) @@ -242,6 +245,13 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext } else if (parameter.ParameterType == typeof(string) || TryParseMethodCache.HasTryParseMethod(parameter)) { + // We're in the fallback case and we have a parameter and route parameter match so don't fallback + // to query string in this case + if (factoryContext.RouteParameters is { } routeParams && routeParams.Contains(parameter.Name)) + { + return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext); + } + return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext); } else @@ -814,6 +824,7 @@ private class FactoryContext public Type? JsonRequestBodyType { get; set; } public bool AllowEmptyRequestBody { get; set; } public IServiceProviderIsService? ServiceProviderIsService { get; init; } + public List? RouteParameters { get; set; } public bool UsingTempSourceString { get; set; } public List<(ParameterExpression, Expression)> TryParseParams { get; } = new(); diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs new file mode 100644 index 000000000000..7381cf010dfb --- /dev/null +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Options for controlling the behavior + /// + public class RequestDelegateFactoryOptions + { + /// + /// The instance used to detect if handler parameters are services. + /// + public IServiceProvider? ServiceProvider { get; init; } + + /// + /// The list of route parameter names that are specified for this handler. + /// + public IEnumerable? RouteParameterNames { get; init; } + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index d782fbca43f5..840319aed9c7 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -98,7 +98,7 @@ public async Task RequestDelegateInvokesAction(Delegate @delegate) { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(@delegate); await requestDelegate(httpContext); @@ -118,7 +118,7 @@ public async Task StaticMethodInfoOverloadWorksWithBasicReflection() BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(HttpContext) }); - var requestDelegate = RequestDelegateFactory.Create(methodInfo!, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(methodInfo!); var httpContext = new DefaultHttpContext(); @@ -163,7 +163,7 @@ object GetTarget() return new TestNonStaticActionClass(2); } - var requestDelegate = RequestDelegateFactory.Create(methodInfo!, new EmptyServiceProvider(), _ => GetTarget()); + var requestDelegate = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); var httpContext = new DefaultHttpContext(); @@ -188,15 +188,11 @@ public void BuildRequestDelegateThrowsArgumentNullExceptions() var serviceProvider = new EmptyServiceProvider(); - var exNullAction = Assert.Throws(() => RequestDelegateFactory.Create(action: null!, serviceProvider)); - var exNullMethodInfo1 = Assert.Throws(() => RequestDelegateFactory.Create(methodInfo: null!, serviceProvider)); - var exNullMethodInfo2 = Assert.Throws(() => RequestDelegateFactory.Create(methodInfo: null!, serviceProvider, _ => 0)); - var exNullTargetFactory = Assert.Throws(() => RequestDelegateFactory.Create(methodInfo!, serviceProvider, targetFactory: null!)); + var exNullAction = Assert.Throws(() => RequestDelegateFactory.Create(action: null!)); + var exNullMethodInfo1 = Assert.Throws(() => RequestDelegateFactory.Create(methodInfo: null!)); Assert.Equal("action", exNullAction.ParamName); Assert.Equal("methodInfo", exNullMethodInfo1.ParamName); - Assert.Equal("methodInfo", exNullMethodInfo2.ParamName); - Assert.Equal("targetFactory", exNullTargetFactory.ParamName); } [Fact] @@ -213,7 +209,7 @@ static void TestAction(HttpContext httpContext, [FromRoute] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -235,12 +231,64 @@ private static void TestOptionalString(HttpContext httpContext, string value = " httpContext.Items.Add("input", value); } + [Fact] + public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() + { + var httpContext = new DefaultHttpContext(); + + var requestDelegate = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => + { + if (id is not null) + { + httpContext.Items["input"] = id; + } + }, + new() { RouteParameterNames = new string[] { "id" } }); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["id"] = "42" + }); + + await requestDelegate(httpContext); + + Assert.Null(httpContext.Items["input"]); + } + + [Fact] + public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() + { + var methodInfo = typeof(HttpHandler).GetMethod(nameof(HttpHandler.Handle)); + + Assert.NotNull(methodInfo); + + var requestDelegate = RequestDelegateFactory.Create(methodInfo!); + var context = new DefaultHttpContext(); + + await requestDelegate(context); + + Assert.Equal(1, context.Items["calls"]); + + await requestDelegate(context); + + Assert.Equal(1, context.Items["calls"]); + } + + [Fact] + public void SpecifiedEmptyRouteParametersThrowIfRouteParameterDoesNotExist() + { + var ex = Assert.Throws(() => + RequestDelegateFactory.Create(([FromRoute] int id) => { }, new() { RouteParameterNames = Array.Empty() })); + + Assert.Equal("id is not a route paramter.", ex.Message); + } + [Fact] public async Task RequestDelegatePopulatesFromRouteOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestOptional); await requestDelegate(httpContext); @@ -252,7 +300,7 @@ public async Task RequestDelegatePopulatesFromNullableOptionalParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestOptional); await requestDelegate(httpContext); @@ -264,7 +312,7 @@ public async Task RequestDelegatePopulatesFromOptionalStringParameter() { var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptionalString, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestOptionalString); await requestDelegate(httpContext); @@ -281,7 +329,7 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParam httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestOptional, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestOptional); await requestDelegate(httpContext); @@ -304,7 +352,7 @@ void TestAction([FromRoute(Name = specifiedName)] int foo) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -327,7 +375,7 @@ void TestAction([FromRoute] int foo) var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues[unmatchedName] = unmatchedRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -406,7 +454,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues["tryParsable"] = routeValue; - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(action); await requestDelegate(httpContext); @@ -423,7 +471,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQ ["tryParsable"] = routeValue }); - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(action); await requestDelegate(httpContext); @@ -442,11 +490,10 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR ["tryParsable"] = "invalid!" }); - var requestDelegate = RequestDelegateFactory.Create((Action)((httpContext, tryParsable) => + var requestDelegate = RequestDelegateFactory.Create((HttpContext httpContext, int tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; - }), - new EmptyServiceProvider()); + }); await requestDelegate(httpContext); @@ -474,7 +521,7 @@ void InvalidFromHeader([FromHeader] object notTryParsable) { } [MemberData(nameof(DelegatesWithAttributesOnNotTryParsableParameters))] public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action) { - var ex = Assert.Throws(() => RequestDelegateFactory.Create(action, new EmptyServiceProvider())); + var ex = Assert.Throws(() => RequestDelegateFactory.Create(action)); Assert.Equal("No public static bool Object.TryParse(string, out Object) method found for notTryParsable.", ex.Message); } @@ -483,7 +530,7 @@ public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument() { var unnamedParameter = Expression.Parameter(typeof(int)); var lambda = Expression.Lambda(Expression.Block(), unnamedParameter); - var ex = Assert.Throws(() => RequestDelegateFactory.Create((Action)lambda.Compile(), new EmptyServiceProvider())); + var ex = Assert.Throws(() => RequestDelegateFactory.Create(lambda.Compile())); Assert.Equal("A parameter does not have a name! Was it generated? All parameters must be named.", ex.Message); } @@ -506,7 +553,7 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -548,7 +595,7 @@ void TestAction([FromQuery] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Query = query; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -571,7 +618,7 @@ void TestAction([FromHeader(Name = customHeaderName)] int value) var httpContext = new DefaultHttpContext(); httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -635,7 +682,7 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) }); httpContext.RequestServices = mock.Object; - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(action); await requestDelegate(httpContext); @@ -652,7 +699,7 @@ public async Task RequestDelegateRejectsEmptyBodyGivenFromBodyParameter(Delegate httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(action); await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } @@ -671,7 +718,7 @@ void TestAction([FromBody(AllowEmpty = true)] Todo todo) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -695,7 +742,7 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -722,7 +769,7 @@ void TestAction([FromBody] Todo todo) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -755,7 +802,7 @@ void TestAction([FromBody] Todo todo) httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -776,9 +823,9 @@ void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { void TestInferredInvalidAction(Todo value1, Todo value2) { } void TestBothInvalidAction(Todo value1, [FromBody] int value2) { } - Assert.Throws(() => RequestDelegateFactory.Create((Action)TestAttributedInvalidAction, new EmptyServiceProvider())); - Assert.Throws(() => RequestDelegateFactory.Create((Action)TestInferredInvalidAction, new EmptyServiceProvider())); - Assert.Throws(() => RequestDelegateFactory.Create((Action)TestBothInvalidAction, new EmptyServiceProvider())); + Assert.Throws(() => RequestDelegateFactory.Create(TestAttributedInvalidAction)); + Assert.Throws(() => RequestDelegateFactory.Create(TestInferredInvalidAction)); + Assert.Throws(() => RequestDelegateFactory.Create(TestBothInvalidAction)); } public static object[][] FromServiceActions @@ -838,7 +885,7 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt var httpContext = new DefaultHttpContext(); httpContext.RequestServices = requestScoped.ServiceProvider; - var requestDelegate = RequestDelegateFactory.Create(action, services); + var requestDelegate = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); await requestDelegate(httpContext); @@ -852,7 +899,7 @@ public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Dele var httpContext = new DefaultHttpContext(); httpContext.RequestServices = new EmptyServiceProvider(); - var requestDelegate = RequestDelegateFactory.Create(action, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(action); await Assert.ThrowsAsync(() => requestDelegate(httpContext)); } @@ -869,7 +916,7 @@ void TestAction(HttpContext httpContext) var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -892,7 +939,7 @@ void TestAction(CancellationToken cancellationToken) RequestAborted = cts.Token }; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -914,7 +961,7 @@ void TestAction(ClaimsPrincipal user) User = new ClaimsPrincipal() }; - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -933,7 +980,7 @@ void TestAction(HttpRequest httpRequest) var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -952,7 +999,7 @@ void TestAction(HttpResponse httpResponse) var httpContext = new DefaultHttpContext(); - var requestDelegate = RequestDelegateFactory.Create((Action)TestAction, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(TestAction); await requestDelegate(httpContext); @@ -996,7 +1043,7 @@ public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Dele var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(@delegate); await requestDelegate(httpContext); @@ -1070,7 +1117,7 @@ public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(@delegate); await requestDelegate(httpContext); @@ -1133,7 +1180,7 @@ public async Task RequestDelegateWritesStringReturnValueAsJsonResponseBody(Deleg var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(@delegate); await requestDelegate(httpContext); @@ -1174,7 +1221,7 @@ public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(@delegate); await requestDelegate(httpContext); @@ -1215,7 +1262,7 @@ public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, new EmptyServiceProvider()); + var requestDelegate = RequestDelegateFactory.Create(@delegate); await requestDelegate(httpContext); @@ -1253,7 +1300,7 @@ public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(D var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, serviceProvider: null); + var requestDelegate = RequestDelegateFactory.Create(@delegate); var exception = await Assert.ThrowsAnyAsync(async () => await requestDelegate(httpContext)); Assert.Contains(message, exception.Message); @@ -1298,7 +1345,7 @@ public async Task RequestDelegateWritesNullReturnNullValue(Delegate @delegate) var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; - var requestDelegate = RequestDelegateFactory.Create(@delegate, serviceProvider: null); + var requestDelegate = RequestDelegateFactory.Create(@delegate); await requestDelegate(httpContext); @@ -1390,6 +1437,17 @@ private class FromServiceAttribute : Attribute, IFromServiceMetadata { } + class HttpHandler + { + private int _calls; + + public void Handle(HttpContext httpContext) + { + _calls++; + httpContext.Items["calls"] = _calls; + } + } + private interface IMyService { } diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 9d8c6a7c33d7..d4220ba76652 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -158,8 +158,20 @@ public static MinimalActionEndpointConventionBuilder Map( const int defaultOrder = 0; + var routeParams = new List(pattern.Parameters.Count); + foreach (var part in pattern.Parameters) + { + routeParams.Add(part.Name); + } + + var options = new RequestDelegateFactoryOptions + { + ServiceProvider = endpoints.ServiceProvider, + RouteParameterNames = routeParams + }; + var builder = new RouteEndpointBuilder( - RequestDelegateFactory.Create(action, endpoints.ServiceProvider), + RequestDelegateFactory.Create(action, options), pattern, defaultOrder) { diff --git a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs index f43d9d97d55d..2d84ff663f2e 100644 --- a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs @@ -7,8 +7,11 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -58,7 +61,7 @@ void TestAction() public void MapGet_BuildsEndpointWithCorrectMethod() { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); - _ = builder.MapGet("/", (Action)(() => { })); + _ = builder.MapGet("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); // Trigger Endpoint build by calling getter. @@ -74,11 +77,57 @@ public void MapGet_BuildsEndpointWithCorrectMethod() Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); } + [Fact] + public async Task MapGetWithRouteParameter_BuildsEndpointWithRouteSpecificBinding() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + _ = builder.MapGet("/{id}", (int? id, HttpContext httpContext) => + { + if (id is not null) + { + httpContext.Items["input"] = id; + } + }); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("GET", method); + + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("/{id} HTTP: GET", routeEndpointBuilder.DisplayName); + Assert.Equal("/{id}", routeEndpointBuilder.RoutePattern.RawText); + + // Assert that we don't fallback to the query string + var httpContext = new DefaultHttpContext(); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["id"] = "42" + }); + + await endpoint.RequestDelegate!(httpContext); + + Assert.Null(httpContext.Items["input"]); + } + + [Fact] + public void MapGetWithRouteParameter_ThrowsIfRouteParameterDoesNotExist() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var ex = Assert.Throws(() => builder.MapGet("/", ([FromRoute] int id) => { })); + Assert.Equal("id is not a route paramter.", ex.Message); + } + [Fact] public void MapPost_BuildsEndpointWithCorrectMethod() { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); - _ = builder.MapPost("/", (Action)(() => { })); + _ = builder.MapPost("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); // Trigger Endpoint build by calling getter. @@ -98,7 +147,7 @@ public void MapPost_BuildsEndpointWithCorrectMethod() public void MapPut_BuildsEndpointWithCorrectMethod() { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); - _ = builder.MapPut("/", (Action)(() => { })); + _ = builder.MapPut("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); // Trigger Endpoint build by calling getter. @@ -118,7 +167,7 @@ public void MapPut_BuildsEndpointWithCorrectMethod() public void MapDelete_BuildsEndpointWithCorrectMethod() { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); - _ = builder.MapDelete("/", (Action)(() => { })); + _ = builder.MapDelete("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); // Trigger Endpoint build by calling getter. @@ -134,6 +183,11 @@ public void MapDelete_BuildsEndpointWithCorrectMethod() Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); } + class FromRoute : Attribute, IFromRouteMetadata + { + public string? Name { get; set; } + } + private class HttpMethodAttribute : Attribute, IHttpMethodMetadata { public bool AcceptCorsPreflight => false;