diff --git a/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs b/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs index de121852ab..a13571bdb8 100644 --- a/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs +++ b/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Routing { @@ -23,8 +24,8 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(httpContext)); } - var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature; - return routingFeature?.RouteData; + var routingFeature = httpContext.Features.Get(); + return routingFeature?.RouteData ?? new RouteData(httpContext.Request.RouteValues); } /// @@ -46,8 +47,7 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(key)); } - var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature; - return routingFeature?.RouteData.Values[key]; + return httpContext.Features.Get()?.RouteValues[key]; } } } diff --git a/src/Http/Routing/perf/EndpointRoutingBenchmarkBase.cs b/src/Http/Routing/perf/EndpointRoutingBenchmarkBase.cs index 0af2021a72..e57a3a9dc5 100644 --- a/src/Http/Routing/perf/EndpointRoutingBenchmarkBase.cs +++ b/src/Http/Routing/perf/EndpointRoutingBenchmarkBase.cs @@ -7,14 +7,12 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Routing { @@ -47,7 +45,7 @@ namespace Microsoft.AspNetCore.Routing return CreateServices().GetRequiredService(); } - private protected static int[] SampleRequests(int endpointCount, int count) + private protected static int[] SampleRequests(int endpointCount, int count) { // This isn't very high tech, but it's at least regular distribution. // We sort the route templates by precedence, so this should result in @@ -131,12 +129,10 @@ namespace Microsoft.AspNetCore.Routing protected (HttpContext httpContext, RouteValueDictionary ambientValues) CreateCurrentRequestContext( object ambientValues = null) { - var feature = new EndpointSelectorContext { RouteValues = new RouteValueDictionary(ambientValues) }; var context = new DefaultHttpContext(); - context.Features.Set(feature); - context.Features.Set(feature); + context.Request.RouteValues = new RouteValueDictionary(ambientValues); - return (context, feature.RouteValues); + return (context, context.Request.RouteValues); } protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint) diff --git a/src/Http/Routing/perf/Matching/MatcherSingleEntryBenchmark.cs b/src/Http/Routing/perf/Matching/MatcherSingleEntryBenchmark.cs index 412ffdaef3..7e8285cade 100644 --- a/src/Http/Routing/perf/Matching/MatcherSingleEntryBenchmark.cs +++ b/src/Http/Routing/perf/Matching/MatcherSingleEntryBenchmark.cs @@ -11,15 +11,11 @@ namespace Microsoft.AspNetCore.Routing.Matching // Just like TechEmpower Plaintext public partial class MatcherSingleEntryBenchmark : EndpointRoutingBenchmarkBase { - private const int SampleCount = 100; - private BarebonesMatcher _baseline; private Matcher _dfa; private Matcher _route; private Matcher _tree; - private EndpointSelectorContext _feature; - [GlobalSetup] public void Setup() { @@ -35,8 +31,6 @@ namespace Microsoft.AspNetCore.Routing.Matching _dfa = SetupMatcher(CreateDfaMatcherBuilder()); _route = SetupMatcher(new RouteMatcherBuilder()); _tree = SetupMatcher(new TreeRouterMatcherBuilder()); - - _feature = new EndpointSelectorContext(); } private Matcher SetupMatcher(MatcherBuilder builder) @@ -48,8 +42,8 @@ namespace Microsoft.AspNetCore.Routing.Matching [Benchmark(Baseline = true)] public async Task Baseline() { - var feature = _feature; var httpContext = Requests[0]; + var feature = new EndpointSelectorContext(httpContext); await _baseline.MatchAsync(httpContext, feature); Validate(httpContext, Endpoints[0], feature.Endpoint); @@ -58,8 +52,8 @@ namespace Microsoft.AspNetCore.Routing.Matching [Benchmark] public async Task Dfa() { - var feature = _feature; var httpContext = Requests[0]; + var feature = new EndpointSelectorContext(httpContext); await _dfa.MatchAsync(httpContext, feature); Validate(httpContext, Endpoints[0], feature.Endpoint); @@ -68,12 +62,8 @@ namespace Microsoft.AspNetCore.Routing.Matching [Benchmark] public async Task LegacyTreeRouter() { - var feature = _feature; - var httpContext = Requests[0]; - - // This is required to make the legacy router implementation work with global routing. - httpContext.Features.Set(feature); + var feature = new EndpointSelectorContext(httpContext); await _tree.MatchAsync(httpContext, feature); Validate(httpContext, Endpoints[0], feature.Endpoint); @@ -82,14 +72,11 @@ namespace Microsoft.AspNetCore.Routing.Matching [Benchmark] public async Task LegacyRouter() { - var feature = _feature; var httpContext = Requests[0]; - - // This is required to make the legacy router implementation work with global routing. - httpContext.Features.Set(feature); + var feature = new EndpointSelectorContext(httpContext); await _route.MatchAsync(httpContext, feature); Validate(httpContext, Endpoints[0], feature.Endpoint); } } -} \ No newline at end of file +} diff --git a/src/Http/Routing/perf/Matching/TrivialMatcher.cs b/src/Http/Routing/perf/Matching/TrivialMatcher.cs index 3e00ce1999..2c4a5fd173 100644 --- a/src/Http/Routing/perf/Matching/TrivialMatcher.cs +++ b/src/Http/Routing/perf/Matching/TrivialMatcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; @@ -30,11 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - var path = httpContext.Request.Path.Value; if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs index 96d44ee48c..0b1214a615 100644 --- a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs +++ b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs @@ -88,11 +88,12 @@ namespace Microsoft.AspNetCore.Routing public EndpointNameMetadata(string endpointName) { } public string EndpointName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } - public sealed partial class EndpointSelectorContext : Microsoft.AspNetCore.Http.Features.IEndpointFeature, Microsoft.AspNetCore.Http.Features.IRouteValuesFeature, Microsoft.AspNetCore.Routing.IRoutingFeature + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct EndpointSelectorContext { - public EndpointSelectorContext() { } - public Microsoft.AspNetCore.Http.Endpoint Endpoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - Microsoft.AspNetCore.Routing.RouteData Microsoft.AspNetCore.Routing.IRoutingFeature.RouteData { get { throw null; } set { } } + private readonly object _dummy; + public EndpointSelectorContext(Microsoft.AspNetCore.Http.HttpContext httpContext) { throw null; } + public Microsoft.AspNetCore.Http.Endpoint Endpoint { get { throw null; } set { } } public Microsoft.AspNetCore.Routing.RouteValueDictionary RouteValues { get { throw null; } set { } } } [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple=false, Inherited=false)] diff --git a/src/Http/Routing/src/EndpointRoutingMiddleware.cs b/src/Http/Routing/src/EndpointRoutingMiddleware.cs index 02b089dbca..93b0f94d08 100644 --- a/src/Http/Routing/src/EndpointRoutingMiddleware.cs +++ b/src/Http/Routing/src/EndpointRoutingMiddleware.cs @@ -6,7 +6,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Logging; @@ -56,7 +55,14 @@ namespace Microsoft.AspNetCore.Routing public Task Invoke(HttpContext httpContext) { - var feature = new EndpointSelectorContext(); + var feature = new EndpointSelectorContext(httpContext); + + // There's already an endpoint, skip maching completely + if (feature.Endpoint != null) + { + Log.MatchSkipped(_logger, feature.Endpoint); + return _next(httpContext); + } // There's an inherent race condition between waiting for init and accessing the matcher // this is OK because once `_matcher` is initialized, it will not be set to null again. @@ -93,31 +99,19 @@ namespace Microsoft.AspNetCore.Routing [MethodImpl(MethodImplOptions.AggressiveInlining)] private Task SetRoutingAndContinue(HttpContext httpContext, EndpointSelectorContext feature) { - if (feature.Endpoint != null) - { - // Set the endpoint feature only on success. This means we won't overwrite any - // existing state for related features unless we did something. - SetFeatures(httpContext, feature); - - Log.MatchSuccess(_logger, feature); - } - else + // If there was no mutation of the endpoint then log failure + if (feature.Endpoint is null) { Log.MatchFailure(_logger); } + else + { + Log.MatchSuccess(_logger, feature); + } return _next(httpContext); } - private static void SetFeatures(HttpContext httpContext, EndpointSelectorContext context) - { - // For back-compat EndpointSelectorContext implements IEndpointFeature, - // IRouteValuesFeature and IRoutingFeature - httpContext.Features.Set(context); - httpContext.Features.Set(context); - httpContext.Features.Set(context); - } - // Initialization is async to avoid blocking threads while reflection and things // of that nature take place. // @@ -185,6 +179,11 @@ namespace Microsoft.AspNetCore.Routing new EventId(2, "MatchFailure"), "Request did not match any endpoints"); + private static readonly Action _matchingSkipped = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3, "MatchingSkipped"), + "Endpoint '{EndpointName}' already set, skipping route matching."); + public static void MatchSuccess(ILogger logger, EndpointSelectorContext context) { _matchSuccess(logger, context.Endpoint.DisplayName, null); @@ -194,6 +193,11 @@ namespace Microsoft.AspNetCore.Routing { _matchFailure(logger, null); } + + public static void MatchSkipped(ILogger logger, Endpoint endpoint) + { + _matchingSkipped(logger, endpoint.DisplayName, null); + } } } } diff --git a/src/Http/Routing/src/EndpointSelectorContext.cs b/src/Http/Routing/src/EndpointSelectorContext.cs index 1aed4f2227..2e30ef54f9 100644 --- a/src/Http/Routing/src/EndpointSelectorContext.cs +++ b/src/Http/Routing/src/EndpointSelectorContext.cs @@ -3,68 +3,49 @@ using System; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Endpoints; namespace Microsoft.AspNetCore.Routing { - public sealed class EndpointSelectorContext : IEndpointFeature, IRouteValuesFeature, IRoutingFeature + public readonly struct EndpointSelectorContext { - private RouteData _routeData; - private RouteValueDictionary _routeValues; + private readonly HttpContext _httpContext; + + public EndpointSelectorContext(HttpContext httpContext) + { + _httpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); + } /// /// Gets or sets the selected for the current /// request. /// - public Endpoint Endpoint { get; set; } + public Endpoint Endpoint + { + get + { + return _httpContext.GetEndpoint(); + } + set + { + _httpContext.SetEndpoint(value); + } + } /// /// Gets or sets the associated with the currrent /// request. /// public RouteValueDictionary RouteValues - { - get => _routeValues ?? (_routeValues = new RouteValueDictionary()); - set - { - _routeValues = value; - - // RouteData will be created next get with new Values - _routeData = null; - } - } - - /// - /// Gets or sets the for the current request. - /// - /// - /// The setter is not implemented. Use to set the route values. - /// - RouteData IRoutingFeature.RouteData { get { - if (_routeData == null) - { - _routeData = _routeValues == null ? new RouteData() : new RouteData(_routeValues); - - // Note: DataTokens won't update if someone else overwrites the Endpoint - // after route values has been set. This seems find since endpoints are a new - // feature and DataTokens are for back-compat. - var dataTokensMetadata = Endpoint?.Metadata.GetMetadata(); - if (dataTokensMetadata != null) - { - var dataTokens = _routeData.DataTokens; - foreach (var kvp in dataTokensMetadata.DataTokens) - { - _routeData.DataTokens.Add(kvp.Key, kvp.Value); - } - } - } - - return _routeData; + return _httpContext.Request.RouteValues; + } + set + { + _httpContext.Request.RouteValues = value; } - set => throw new NotSupportedException(); } } } diff --git a/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs b/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs index 657621b759..2ef8015753 100644 --- a/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs +++ b/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs @@ -21,11 +21,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - if (candidateSet == null) { throw new ArgumentNullException(nameof(candidateSet)); diff --git a/src/Http/Routing/src/Matching/DfaMatcher.cs b/src/Http/Routing/src/Matching/DfaMatcher.cs index e41fc44f6d..b39ca762a9 100644 --- a/src/Http/Routing/src/Matching/DfaMatcher.cs +++ b/src/Http/Routing/src/Matching/DfaMatcher.cs @@ -34,11 +34,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - // All of the logging we do here is at level debug, so we can get away with doing a single check. var log = _logger.IsEnabled(LogLevel.Debug); diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs index f99afdcfbd..4f52eb3932 100644 --- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs @@ -77,11 +77,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 3111c55a67..4a6d17344f 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -94,11 +94,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); diff --git a/src/Http/Routing/src/RouterMiddleware.cs b/src/Http/Routing/src/RouterMiddleware.cs index aa46d4f570..0190cd468f 100644 --- a/src/Http/Routing/src/RouterMiddleware.cs +++ b/src/Http/Routing/src/RouterMiddleware.cs @@ -40,11 +40,15 @@ namespace Microsoft.AspNetCore.Builder } else { - httpContext.Features[typeof(IRoutingFeature)] = new RoutingFeature() + var routingFeature = new RoutingFeature() { - RouteData = context.RouteData, + RouteData = context.RouteData }; + // Set the RouteValues on the current request, this is to keep the IRouteValuesFeature inline with the IRoutingFeature + httpContext.Request.RouteValues = context.RouteData.Values; + httpContext.Features.Set(routingFeature); + await context.Handler(context.HttpContext); } } diff --git a/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs b/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs index ef05217153..a6b288d0e9 100644 --- a/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs +++ b/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs @@ -635,12 +635,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(endpointControllerAction, endpointController, endpointEmpty, endpointControllerActionParameter); - var context = new EndpointSelectorContext() - { - RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index", }) - }; var httpContext = CreateHttpContext(); - httpContext.Features.Set(context); + httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }); var values = new RouteValueDictionary(); for (int i = 0; i < routeNames.Length; i++) @@ -678,12 +674,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); - var context = new EndpointSelectorContext() - { - RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index", }) - }; var httpContext = CreateHttpContext(); - httpContext.Features.Set(context); + httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index", }); var values = new RouteValueDictionary(); for (int i = 0; i < routeNames.Length; i++) @@ -721,9 +713,7 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); - var context = new EndpointSelectorContext(); var httpContext = CreateHttpContext(); - httpContext.Features.Set(context); var values = new RouteValueDictionary(); for (int i = 0; i < routeNames.Length; i++) diff --git a/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs b/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs index 17832a6b48..4d0cafb81b 100644 --- a/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs +++ b/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -44,12 +45,8 @@ namespace Microsoft.AspNetCore.Routing // Arrange var httpContext = new DefaultHttpContext(); httpContext.RequestServices = new ServiceProvider(); - - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = null, - }); - + httpContext.SetEndpoint(null); + RequestDelegate next = (c) => { return Task.CompletedTask; @@ -77,11 +74,8 @@ namespace Microsoft.AspNetCore.Routing return Task.CompletedTask; }; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint(endpointFunc, EndpointMetadataCollection.Empty, "Test"), - }); - + httpContext.SetEndpoint(new Endpoint(endpointFunc, EndpointMetadataCollection.Empty, "Test")); + RequestDelegate next = (c) => { return Task.CompletedTask; @@ -108,10 +102,7 @@ namespace Microsoft.AspNetCore.Routing RequestServices = new ServiceProvider() }; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test"), - }); + httpContext.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test")); var middleware = new EndpointMiddleware(NullLogger.Instance, _ => Task.CompletedTask, RouteOptions); @@ -131,10 +122,7 @@ namespace Microsoft.AspNetCore.Routing RequestServices = new ServiceProvider() }; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test"), - }); + httpContext.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test")); httpContext.Items[EndpointMiddleware.AuthorizationMiddlewareInvokedKey] = true; @@ -155,10 +143,8 @@ namespace Microsoft.AspNetCore.Routing RequestServices = new ServiceProvider() }; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test"), - }); + httpContext.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test")); + var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true }); var middleware = new EndpointMiddleware(NullLogger.Instance, _ => Task.CompletedTask, routeOptions); @@ -178,10 +164,7 @@ namespace Microsoft.AspNetCore.Routing RequestServices = new ServiceProvider() }; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test"), - }); + httpContext.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test")); var middleware = new EndpointMiddleware(NullLogger.Instance, _ => Task.CompletedTask, RouteOptions); @@ -201,10 +184,7 @@ namespace Microsoft.AspNetCore.Routing RequestServices = new ServiceProvider() }; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test"), - }); + httpContext.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test")); httpContext.Items[EndpointMiddleware.CorsMiddlewareInvokedKey] = true; @@ -225,10 +205,8 @@ namespace Microsoft.AspNetCore.Routing RequestServices = new ServiceProvider() }; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test"), - }); + httpContext.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of()), "Test")); + var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true }); var middleware = new EndpointMiddleware(NullLogger.Instance, _ => Task.CompletedTask, routeOptions); diff --git a/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs b/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs index 97f33a2fbe..88b5de1555 100644 --- a/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs +++ b/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs @@ -2,17 +2,16 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -36,6 +35,24 @@ namespace Microsoft.AspNetCore.Routing Assert.NotNull(endpointFeature); } + [Fact] + public async Task Invoke_SkipsRouting_IfEndpointSet() + { + // Arrange + var httpContext = CreateHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "myapp")); + + var middleware = CreateMiddleware(); + + // Act + await middleware.Invoke(httpContext); + + // Assert + var endpoint = httpContext.GetEndpoint(); + Assert.NotNull(endpoint); + Assert.Equal("myapp", endpoint.DisplayName); + } + [Fact] public async Task Invoke_OnCall_WritesToConfiguredLogger() { @@ -132,25 +149,22 @@ namespace Microsoft.AspNetCore.Routing private HttpContext CreateHttpContext() { - var context = new EndpointSelectorContext(); - - var httpContext = new DefaultHttpContext(); - httpContext.Features.Set(context); - httpContext.Features.Set(context); - - httpContext.RequestServices = new TestServiceProvider(); + var httpContext = new DefaultHttpContext + { + RequestServices = new TestServiceProvider() + }; return httpContext; } private EndpointRoutingMiddleware CreateMiddleware( Logger logger = null, - MatcherFactory matcherFactory = null) + MatcherFactory matcherFactory = null, + RequestDelegate next = null) { - RequestDelegate next = (c) => Task.FromResult(null); - - logger = logger ?? new Logger(NullLoggerFactory.Instance); - matcherFactory = matcherFactory ?? new TestMatcherFactory(true); + next ??= c => Task.CompletedTask; + logger ??= new Logger(NullLoggerFactory.Instance); + matcherFactory ??= new TestMatcherFactory(true); var middleware = new EndpointRoutingMiddleware( matcherFactory, diff --git a/src/Http/Routing/test/UnitTests/EndpointSelectorContextTest.cs b/src/Http/Routing/test/UnitTests/EndpointSelectorContextTest.cs index 05ca8850da..833d2697d0 100644 --- a/src/Http/Routing/test/UnitTests/EndpointSelectorContextTest.cs +++ b/src/Http/Routing/test/UnitTests/EndpointSelectorContextTest.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. -using System.Linq; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; @@ -12,48 +11,43 @@ namespace Microsoft.AspNetCore.Routing public class EndpointSelectorContextTest { [Fact] - public void RouteData_CanIntializeDataTokens_WithMetadata() + public void SettingEndpointSetsEndpointOnHttpContext() { - // Arrange - var expected = new RouteValueDictionary(new { foo = 17, bar = "hello", }); - - var context = new EndpointSelectorContext() - { - Endpoint = new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/"), - 0, - new EndpointMetadataCollection(new DataTokensMetadata(expected)), - "test"), - }; - - // Act - var routeData = ((IRoutingFeature)context).RouteData; - - // Assert - Assert.NotSame(expected, routeData.DataTokens); - Assert.Equal(expected.OrderBy(kvp => kvp.Key), routeData.DataTokens.OrderBy(kvp => kvp.Key)); - } - - [Fact] - public void RouteData_DataTokensIsEmpty_WithoutMetadata() - { - // Arrange - var context = new EndpointSelectorContext() - { - Endpoint = new RouteEndpoint( + var httpContext = new DefaultHttpContext(); + var ep = new RouteEndpoint( TestConstants.EmptyRequestDelegate, RoutePatternFactory.Parse("/"), 0, new EndpointMetadataCollection(), - "test"), + "test"); + + new EndpointSelectorContext(httpContext) + { + Endpoint = ep, }; - // Act - var routeData = ((IRoutingFeature)context).RouteData; + // Assert + var endpoint = httpContext.GetEndpoint(); + Assert.NotNull(endpoint); + Assert.Same(ep, endpoint); + } + + [Fact] + public void SettingRouteValuesSetRouteValuesHttpContext() + { + var httpContext = new DefaultHttpContext(); + var routeValues = new RouteValueDictionary(new { A = "1" }); + + new EndpointSelectorContext(httpContext) + { + RouteValues = routeValues + }; // Assert - Assert.Empty(routeData.DataTokens); + Assert.NotNull(httpContext.Request.RouteValues); + Assert.Same(routeValues, httpContext.Request.RouteValues); + Assert.Single(httpContext.Request.RouteValues); + Assert.Equal("1", httpContext.Request.RouteValues["A"]); } } } diff --git a/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs b/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs index e2a9382281..99ec7cb107 100644 --- a/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System.Linq; @@ -26,12 +26,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - var context = new EndpointSelectorContext() - { - RouteValues = new RouteValueDictionary(new { p = "5", }) - }; var httpContext = CreateHttpContext(); - httpContext.Features.Set(context); + httpContext.Request.RouteValues = new RouteValueDictionary(new { p = "5", }); httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); var values = new { query = "some?query", }; diff --git a/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs b/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs index 5f8d509d54..181bb75601 100644 --- a/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System.Linq; @@ -32,12 +32,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - var context = new EndpointSelectorContext() - { - RouteValues = new RouteValueDictionary(new { action = "Index", }) - }; var httpContext = CreateHttpContext(); - httpContext.Features.Set(context); + httpContext.Request.RouteValues = new RouteValueDictionary(new { action = "Index", }); httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); // Act diff --git a/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs b/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs index 19ded38c8b..579a0000ae 100644 --- a/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs +++ b/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs @@ -18,14 +18,7 @@ namespace Microsoft.AspNetCore.Routing protected HttpContext CreateHttpContext(object ambientValues = null) { var httpContext = new DefaultHttpContext(); - - var context = new EndpointSelectorContext - { - RouteValues = new RouteValueDictionary(ambientValues) - }; - - httpContext.Features.Set(context); - httpContext.Features.Set(context); + httpContext.Request.RouteValues = new RouteValueDictionary(ambientValues); return httpContext; } diff --git a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs index f1501b423f..87eecf6b0f 100644 --- a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs +++ b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; @@ -27,11 +27,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - var path = httpContext.Request.Path.Value; for (var i = 0; i < Matchers.Length; i++) { @@ -133,4 +128,4 @@ namespace Microsoft.AspNetCore.Routing.Matching } } } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs b/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs index 9d67c126d6..65b5660930 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; @@ -174,12 +174,8 @@ namespace Microsoft.AspNetCore.Routing.Matching private static (HttpContext httpContext, EndpointSelectorContext context) CreateContext() { - var context = new EndpointSelectorContext(); var httpContext = new DefaultHttpContext(); - httpContext.Features.Set(context); - httpContext.Features.Set(context); - - return (httpContext, context); + return (httpContext, new EndpointSelectorContext(httpContext)); } private static RouteEndpoint CreateEndpoint(string template) diff --git a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs index 52e6dfdfeb..ff706040c9 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs @@ -403,7 +403,7 @@ namespace Microsoft.AspNetCore.Routing.Matching var endpointSelector = new Mock(); endpointSelector .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((c, f, cs) => + .Callback((c, f, cs) => { Assert.Equal(2, cs.Count); @@ -449,7 +449,7 @@ namespace Microsoft.AspNetCore.Routing.Matching var endpointSelector = new Mock(); endpointSelector .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((c, f, cs) => + .Callback((c, f, cs) => { Assert.Equal(2, cs.Count); @@ -496,7 +496,7 @@ namespace Microsoft.AspNetCore.Routing.Matching var endpointSelector = new Mock(); endpointSelector .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((c, f, cs) => + .Callback((c, f, cs) => { Assert.Equal(2, cs.Count); @@ -727,7 +727,7 @@ namespace Microsoft.AspNetCore.Routing.Matching var (httpContext, context) = CreateContext(); httpContext.Request.Path = "/test/17"; - + // Act await matcher.MatchAsync(httpContext, context); @@ -821,13 +821,9 @@ namespace Microsoft.AspNetCore.Routing.Matching private (HttpContext httpContext, EndpointSelectorContext context) CreateContext() { - var context = new EndpointSelectorContext(); - var httpContext = new DefaultHttpContext(); - httpContext.Features.Set(context); - httpContext.Features.Set(context); - return (httpContext, context); + return (httpContext, new EndpointSelectorContext(httpContext)); } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs index c812ad8d4c..8fdef05eef 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs @@ -303,11 +303,7 @@ namespace Microsoft.AspNetCore.Routing.Matching httpContext.Request.Path = path; httpContext.Request.Scheme = scheme; - var context = new EndpointSelectorContext(); - httpContext.Features.Set(context); - httpContext.Features.Set(context); - - return (httpContext, context); + return (httpContext, new EndpointSelectorContext(httpContext)); } internal RouteEndpoint CreateEndpoint( diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs index 258e9b0a92..6009c07d54 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs @@ -352,11 +352,7 @@ namespace Microsoft.AspNetCore.Routing.Matching httpContext.Request.Headers[AccessControlRequestMethod] = httpMethod; } - var context = new EndpointSelectorContext(); - httpContext.Features.Set(context); - httpContext.Features.Set(context); - - return (httpContext, context); + return (httpContext, new EndpointSelectorContext(httpContext)); } internal RouteEndpoint CreateEndpoint( diff --git a/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs b/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs index 28ec4c68a4..8f634f1a34 100644 --- a/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs +++ b/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs @@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new XunitException($"Was expected to match '{expected.DisplayName}' but did not match."); } - var actualValues = httpContext.Features.Get().RouteValues; + var actualValues = httpContext.Request.RouteValues; if (actualValues == null) { diff --git a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs index c7615bc19c..df0cdaf910 100644 --- a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; @@ -20,15 +20,7 @@ namespace Microsoft.AspNetCore.Routing.Matching httpContext.Request.Method = "TEST"; httpContext.Request.Path = path; httpContext.RequestServices = CreateServices(); - - var context = new EndpointSelectorContext() - { - RouteValues = new RouteValueDictionary() - }; - httpContext.Features.Set(context); - httpContext.Features.Set(context); - - return (httpContext, context); + return (httpContext, new EndpointSelectorContext(httpContext)); } // The older routing implementations retrieve services when they first execute. diff --git a/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs b/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs index 39eaee6cdd..24b4a006cb 100644 --- a/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs +++ b/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; @@ -24,11 +24,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - var routeContext = new RouteContext(httpContext); await _inner.RouteAsync(routeContext); diff --git a/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs b/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs index da068f5198..058a7f40d9 100644 --- a/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs +++ b/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs @@ -1,10 +1,11 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; @@ -95,10 +96,9 @@ namespace Microsoft.AspNetCore.Routing.Matching public async Task RouteAsync(RouteContext routeContext) { - var context = (EndpointSelectorContext)routeContext.HttpContext.Features.Get(); - - // This is needed due to a quirk of our tests - they reuse the endpoint feature - // across requests. + var context = new EndpointSelectorContext(routeContext.HttpContext); + + // This is needed due to a quirk of our tests - they reuse the endpoint feature. context.Endpoint = null; await _selector.SelectAsync(routeContext.HttpContext, context, new CandidateSet(_candidates, _values, _scores)); diff --git a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs index c7477e4611..ec1c60105a 100644 --- a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs +++ b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; @@ -26,11 +26,6 @@ namespace Microsoft.AspNetCore.Routing.Matching throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - var routeContext = new RouteContext(httpContext); await _inner.RouteAsync(routeContext); diff --git a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs index 5d4e25dfff..572308e9a4 100644 --- a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs +++ b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs @@ -99,11 +99,11 @@ namespace Microsoft.AspNetCore.Routing.Matching public async Task RouteAsync(RouteContext routeContext) { - var context = (EndpointSelectorContext)routeContext.HttpContext.Features.Get(); + var context = new EndpointSelectorContext(routeContext.HttpContext); // This is needed due to a quirk of our tests - they reuse the endpoint feature. context.Endpoint = null; - + await _selector.SelectAsync(routeContext.HttpContext, context, new CandidateSet(_candidates, _values, _scores)); if (context.Endpoint != null) { diff --git a/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs b/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs index 3df8f6ad48..29a03c7e08 100644 --- a/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs +++ b/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs @@ -5,13 +5,65 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Routing { public class RouterMiddlewareTest { + [Fact] + public async Task RoutingFeatureSetInIRouter() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + var httpContext = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider() + }; + + httpContext.Request.Path = "/foo/10"; + + var routeHandlerExecuted = false; + + var handler = new RouteHandler(context => + { + routeHandlerExecuted = true; + + var routingFeature = context.Features.Get(); + + Assert.NotNull(routingFeature); + Assert.NotNull(context.Features.Get()); + + Assert.Single(routingFeature.RouteData.Values); + Assert.Single(context.Request.RouteValues); + Assert.True(routingFeature.RouteData.Values.ContainsKey("id")); + Assert.True(context.Request.RouteValues.ContainsKey("id")); + Assert.Equal("10", routingFeature.RouteData.Values["id"]); + Assert.Equal("10", context.Request.RouteValues["id"]); + Assert.Equal("10", context.GetRouteValue("id")); + Assert.Same(routingFeature.RouteData, context.GetRouteData()); + + return Task.CompletedTask; + }); + + var route = new Route(handler, "/foo/{id}", Mock.Of()); + + var middleware = new RouterMiddleware(context => Task.CompletedTask, NullLoggerFactory.Instance, route); + + // Act + await middleware.Invoke(httpContext); + + // Assert + Assert.True(routeHandlerExecuted); + + } + [Fact] public async Task Invoke_LogsCorrectValues_WhenNotHandled() { diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs b/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs index 35e4e44c01..4f2d60bc76 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System; diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs b/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs index 1e866ea27e..f355ba7835 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs @@ -173,7 +173,7 @@ namespace RoutingWebSite app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapGet("api/get/{id}", (context) => context.Response.WriteAsync($"{name} - API Get {context.GetRouteData().Values["id"]}")); + endpoints.MapGet("api/get/{id}", (context) => context.Response.WriteAsync($"{name} - API Get {context.Request.RouteValues["id"]}")); }); } } diff --git a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs index d5be158b25..bbbd974d83 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs @@ -286,7 +286,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing RequestDelegate requestDelegate = (context) => { - var routeData = context.GetRouteData(); + var routeData = new RouteData(); + routeData.PushState(router: null, context.Request.RouteValues, dataTokens); + var actionContext = new ActionContext(context, routeData, action); if (invokerFactory == null) diff --git a/src/Mvc/Mvc.Core/src/Routing/ConsumesMatcherPolicy.cs b/src/Mvc/Mvc.Core/src/Routing/ConsumesMatcherPolicy.cs index ca1ce032a9..dbe36b5d6b 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ConsumesMatcherPolicy.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ConsumesMatcherPolicy.cs @@ -60,11 +60,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); diff --git a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs index 0dcc828c5f..765ffac3ca 100644 --- a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs +++ b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs @@ -66,12 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing { throw new ArgumentNullException(nameof(httpContext)); } - - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); diff --git a/src/Mvc/Mvc.Core/test/Routing/ConsumesMatcherPolicyTest.cs b/src/Mvc/Mvc.Core/test/Routing/ConsumesMatcherPolicyTest.cs index 7d63eaacc4..564fa00bcf 100644 --- a/src/Mvc/Mvc.Core/test/Routing/ConsumesMatcherPolicyTest.cs +++ b/src/Mvc/Mvc.Core/test/Routing/ConsumesMatcherPolicyTest.cs @@ -532,7 +532,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing }; var candidates = CreateCandidateSet(endpoints); - var context = new EndpointSelectorContext(); var httpContext = new DefaultHttpContext() { Request = @@ -540,6 +539,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing ContentType = "application/json", }, }; + var context = new EndpointSelectorContext(httpContext); var policy = CreatePolicy(); @@ -562,7 +562,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing }; var candidates = CreateCandidateSet(endpoints); - var context = new EndpointSelectorContext(); var httpContext = new DefaultHttpContext() { Request = @@ -570,6 +569,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing ContentType = "application/json", }, }; + var context = new EndpointSelectorContext(httpContext); var policy = CreatePolicy(); @@ -592,7 +592,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing }; var candidates = CreateCandidateSet(endpoints); - var context = new EndpointSelectorContext(); var httpContext = new DefaultHttpContext() { Request = @@ -600,6 +599,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing ContentType = "application/json", }, }; + var context = new EndpointSelectorContext(httpContext); var policy = CreatePolicy(); diff --git a/src/Mvc/Mvc.Core/test/Routing/ControllerLinkGeneratorExtensionsTest.cs b/src/Mvc/Mvc.Core/test/Routing/ControllerLinkGeneratorExtensionsTest.cs index 5b6663ba86..061a78c630 100644 --- a/src/Mvc/Mvc.Core/test/Routing/ControllerLinkGeneratorExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/Routing/ControllerLinkGeneratorExtensionsTest.cs @@ -209,14 +209,7 @@ namespace Microsoft.AspNetCore.Routing private HttpContext CreateHttpContext(object ambientValues = null) { var httpContext = new DefaultHttpContext(); - - var feature = new EndpointSelectorContext - { - RouteValues = new RouteValueDictionary(ambientValues) - }; - - httpContext.Features.Set(feature); - httpContext.Features.Set(feature); + httpContext.Request.RouteValues = new RouteValueDictionary(ambientValues); return httpContext; } } diff --git a/src/Mvc/Mvc.Core/test/Routing/EndpointRoutingUrlHelperTest.cs b/src/Mvc/Mvc.Core/test/Routing/EndpointRoutingUrlHelperTest.cs index 0f85a857fa..ee0b81571e 100644 --- a/src/Mvc/Mvc.Core/test/Routing/EndpointRoutingUrlHelperTest.cs +++ b/src/Mvc/Mvc.Core/test/Routing/EndpointRoutingUrlHelperTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; @@ -59,12 +60,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing routeName: "OrdersApi"); var urlHelper = CreateUrlHelper(new[] { endpoint1, endpoint2 }); - // Set the endpoint feature and current context just as a normal request to MVC app would be - var endpointFeature = new EndpointSelectorContext(); - urlHelper.ActionContext.HttpContext.Features.Set(endpointFeature); - urlHelper.ActionContext.HttpContext.Features.Set(endpointFeature); - endpointFeature.Endpoint = endpoint1; - endpointFeature.RouteValues = new RouteValueDictionary + urlHelper.ActionContext.HttpContext.SetEndpoint(endpoint1); + urlHelper.ActionContext.HttpContext.Request.RouteValues = new RouteValueDictionary { ["controller"] = "Orders", ["action"] = "GetById", @@ -149,13 +146,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing protected override IUrlHelper CreateUrlHelper(ActionContext actionContext) { var httpContext = actionContext.HttpContext; - httpContext.Features.Set(new EndpointSelectorContext() - { - Endpoint = new Endpoint( - context => Task.CompletedTask, - EndpointMetadataCollection.Empty, - null) - }); + httpContext.SetEndpoint(new Endpoint(context => Task.CompletedTask, EndpointMetadataCollection.Empty, null)); var urlHelperFactory = httpContext.RequestServices.GetRequiredService(); var urlHelper = urlHelperFactory.GetUrlHelper(actionContext); diff --git a/src/Mvc/Mvc.Core/test/Routing/PageLinkGeneratorExtensionsTest.cs b/src/Mvc/Mvc.Core/test/Routing/PageLinkGeneratorExtensionsTest.cs index 70f7a17221..0d1b66e2c1 100644 --- a/src/Mvc/Mvc.Core/test/Routing/PageLinkGeneratorExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/Routing/PageLinkGeneratorExtensionsTest.cs @@ -207,14 +207,7 @@ namespace Microsoft.AspNetCore.Routing private HttpContext CreateHttpContext(object ambientValues = null) { var httpContext = new DefaultHttpContext(); - - var feature = new EndpointSelectorContext - { - RouteValues = new RouteValueDictionary(ambientValues) - }; - - httpContext.Features.Set(feature); - httpContext.Features.Set(feature); + httpContext.Request.RouteValues = new RouteValueDictionary(ambientValues); return httpContext; } } diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs index 2e108ccf66..8f8a4eedae 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs @@ -68,11 +68,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs index d8b274e826..d3beefd714 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs @@ -58,11 +58,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure throw new ArgumentNullException(nameof(httpContext)); } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - if (candidates == null) { throw new ArgumentNullException(nameof(candidates)); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs index a917555871..637d6e086f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; @@ -18,16 +19,18 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal partial class HttpProtocol : IHttpRequestFeature, - IHttpResponseFeature, - IResponseBodyPipeFeature, - IRequestBodyPipeFeature, - IHttpUpgradeFeature, - IHttpConnectionFeature, - IHttpRequestLifetimeFeature, - IHttpRequestIdentifierFeature, - IHttpBodyControlFeature, - IHttpMaxRequestBodySizeFeature, - IHttpResponseStartFeature + IHttpResponseFeature, + IResponseBodyPipeFeature, + IRequestBodyPipeFeature, + IHttpUpgradeFeature, + IHttpConnectionFeature, + IHttpRequestLifetimeFeature, + IHttpRequestIdentifierFeature, + IHttpBodyControlFeature, + IHttpMaxRequestBodySizeFeature, + IHttpResponseStartFeature, + IEndpointFeature, + IRouteValuesFeature { // NOTE: When feature interfaces are added to or removed from this HttpProtocol class implementation, // then the list of `implementedFeatures` in the generated code project MUST also be updated. @@ -258,6 +261,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + Endpoint IEndpointFeature.Endpoint + { + get => _endpoint; + set => _endpoint = value; + } + + RouteValueDictionary IRouteValuesFeature.RouteValues + { + get => _routeValues ??= new RouteValueDictionary(); + set => _routeValues = value; + } + protected void ResetHttp1Features() { _currentIHttpMinRequestBodyDataRateFeature = this; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs index bd0edf1357..aaeffffe1b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs @@ -21,6 +21,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private static readonly Type IServiceProvidersFeatureType = typeof(IServiceProvidersFeature); private static readonly Type IHttpRequestLifetimeFeatureType = typeof(IHttpRequestLifetimeFeature); private static readonly Type IHttpConnectionFeatureType = typeof(IHttpConnectionFeature); + private static readonly Type IRouteValuesFeatureType = typeof(IRouteValuesFeature); + private static readonly Type IEndpointFeatureType = typeof(IEndpointFeature); private static readonly Type IHttpAuthenticationFeatureType = typeof(IHttpAuthenticationFeature); private static readonly Type IQueryFeatureType = typeof(IQueryFeature); private static readonly Type IFormFeatureType = typeof(IFormFeature); @@ -47,6 +49,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private object _currentIServiceProvidersFeature; private object _currentIHttpRequestLifetimeFeature; private object _currentIHttpConnectionFeature; + private object _currentIRouteValuesFeature; + private object _currentIEndpointFeature; private object _currentIHttpAuthenticationFeature; private object _currentIQueryFeature; private object _currentIFormFeature; @@ -82,6 +86,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _currentIHttpMaxRequestBodySizeFeature = this; _currentIHttpBodyControlFeature = this; _currentIHttpResponseStartFeature = this; + _currentIRouteValuesFeature = this; + _currentIEndpointFeature = this; _currentIServiceProvidersFeature = null; _currentIHttpAuthenticationFeature = null; @@ -183,6 +189,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { feature = _currentIHttpConnectionFeature; } + else if (key == IRouteValuesFeatureType) + { + feature = _currentIRouteValuesFeature; + } + else if (key == IEndpointFeatureType) + { + feature = _currentIEndpointFeature; + } else if (key == IHttpAuthenticationFeatureType) { feature = _currentIHttpAuthenticationFeature; @@ -295,6 +309,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _currentIHttpConnectionFeature = value; } + else if (key == IRouteValuesFeatureType) + { + _currentIRouteValuesFeature = value; + } + else if (key == IEndpointFeatureType) + { + _currentIEndpointFeature = value; + } else if (key == IHttpAuthenticationFeatureType) { _currentIHttpAuthenticationFeature = value; @@ -405,6 +427,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { feature = (TFeature)_currentIHttpConnectionFeature; } + else if (typeof(TFeature) == typeof(IRouteValuesFeature)) + { + feature = (TFeature)_currentIRouteValuesFeature; + } + else if (typeof(TFeature) == typeof(IEndpointFeature)) + { + feature = (TFeature)_currentIEndpointFeature; + } else if (typeof(TFeature) == typeof(IHttpAuthenticationFeature)) { feature = (TFeature)_currentIHttpAuthenticationFeature; @@ -521,6 +551,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _currentIHttpConnectionFeature = feature; } + else if (typeof(TFeature) == typeof(IRouteValuesFeature)) + { + _currentIRouteValuesFeature = feature; + } + else if (typeof(TFeature) == typeof(IEndpointFeature)) + { + _currentIEndpointFeature = feature; + } else if (typeof(TFeature) == typeof(IHttpAuthenticationFeature)) { _currentIHttpAuthenticationFeature = feature; @@ -629,6 +667,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { yield return new KeyValuePair(IHttpConnectionFeatureType, _currentIHttpConnectionFeature); } + if (_currentIRouteValuesFeature != null) + { + yield return new KeyValuePair(IRouteValuesFeatureType, _currentIRouteValuesFeature); + } + if (_currentIEndpointFeature != null) + { + yield return new KeyValuePair(IEndpointFeatureType, _currentIEndpointFeature); + } if (_currentIHttpAuthenticationFeature != null) { yield return new KeyValuePair(IHttpAuthenticationFeatureType, _currentIHttpAuthenticationFeature); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index e7d2dd154b..164bb14877 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -64,6 +65,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private readonly HttpConnectionContext _context; private DefaultHttpContext _httpContext; + private RouteValueDictionary _routeValues; + private Endpoint _endpoint; protected string _methodText = null; private string _scheme = null; @@ -326,6 +329,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _onStarting?.Clear(); _onCompleted?.Clear(); + _routeValues?.Clear(); _requestProcessingStatus = RequestProcessingStatus.RequestPending; _autoChunk = false; @@ -340,6 +344,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http TraceIdentifier = null; Method = HttpMethod.None; _methodText = null; + _endpoint = null; PathBase = null; Path = null; RawTarget = null; diff --git a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs index 28b1df7c7a..2524e88785 100644 --- a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs @@ -127,6 +127,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _collection[typeof(IHttpMinResponseDataRateFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpBodyControlFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpResponseStartFeature)] = CreateHttp1Connection(); + _collection[typeof(IRouteValuesFeature)] = CreateHttp1Connection(); + _collection[typeof(IEndpointFeature)] = CreateHttp1Connection(); CompareGenericGetterToIndexer(); @@ -148,6 +150,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); + _collection.Set(CreateHttp1Connection()); + _collection.Set(CreateHttp1Connection()); CompareGenericGetterToIndexer(); diff --git a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs index f61df14ff2..1be680d38a 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs @@ -19,6 +19,8 @@ namespace CodeGenerator "IServiceProvidersFeature", "IHttpRequestLifetimeFeature", "IHttpConnectionFeature", + "IRouteValuesFeature", + "IEndpointFeature" }; var commonFeatures = new[] @@ -70,7 +72,9 @@ namespace CodeGenerator "IHttpConnectionFeature", "IHttpMaxRequestBodySizeFeature", "IHttpBodyControlFeature", - "IHttpResponseStartFeature" + "IHttpResponseStartFeature", + "IRouteValuesFeature", + "IEndpointFeature" }; var usings = $@"