From acd1cf3251df12259900f82a4632425dd9cc128c Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 11 Feb 2019 19:18:05 -0800 Subject: [PATCH] Update MVC/Routing Startup Experience (#7425) * Relayer MvcEndpointDataSource Separates the statefulness of the data source from the business logic of how endpoints are created. I'm separating these concerns because one of the next steps will split the MvcEndpointDataSource into two data sources. * Simplify MvcEndpointInfo Removing things that are unused and leftovers from the 2.2 design of this feature. * Remove per-route conventions Removes the ability to target endpoint conventions per-conventional-route. This was a neat idea but we have no plans to ship it for now. Simplified MvcEndpointInfo and renamed it to reflect its new purpose. * Remove filtering from MvcEndpointDataSource This was neat-o but we're not going to ship it like this. We're going to implement filtering in another place. Putting this in the data source is pretty clumsy and doesn't work with features like application parts that need to be baked in addservices * Simplify ActionEndpointFactory * Split up data sources * Use UseRouting in functional tests I've rejiggered our functional tests to de-emphasize UseMvc(...) and only use it when we're specifically testing the old scenarios. UseMvc(...) won't appear in templates in 3.0 so it's legacy. * Update templates * Add minor PR feedback * one more --- ...s => ActionEndpointDatasourceBenchmark.cs} | 34 +- src/Mvc/samples/MvcSandbox/Startup.cs | 3 +- ...ontrollerEndpointRouteBuilderExtensions.cs | 179 ++++ .../DefaultEndpointConventionBuilder.cs | 24 - .../MvcApplicationBuilderExtensions.cs | 21 +- .../Builder/MvcEndpointInfo.cs | 79 -- .../MvcEndpointRouteBuilderExtensions.cs | 121 --- .../MvcCoreServiceCollectionExtensions.cs | 4 +- .../Routing/ActionEndpointDataSource.cs | 65 ++ .../Routing/ActionEndpointDataSourceBase.cs | 136 +++ .../Routing/ActionEndpointFactory.cs | 278 ++++++ .../ControllerActionEndpointDataSource.cs | 54 ++ .../Routing/ConventionalRouteEntry.cs | 56 ++ .../Routing/MvcEndpointDataSource.cs | 381 --------- ...azorPagesEndpointRouteBuilderExtensions.cs | 48 +- .../MvcRazorPagesMvcCoreBuilderExtensions.cs | 1 + .../PageActionEndpointDataSource.cs | 43 + .../MvcApplicationBuilderExtensionsTest.cs | 12 +- .../ActionEndpointDataSourceBaseTest.cs | 173 ++++ .../Routing/ActionEndpointDataSourceTest.cs | 173 ++++ .../Routing/ActionEndpointFactoryTest.cs | 498 +++++++++++ .../ControllerActionEndpointDataSourceTest.cs | 206 +++++ .../Routing/MvcEndpointDataSourceTests.cs | 806 ------------------ .../AntiforgeryTests.cs | 4 +- .../ApiBehaviorTest.cs | 4 +- .../AsyncActionsTests.cs | 4 +- .../BasicTests.cs | 4 +- .../ComponentRenderingFunctionalTests.cs | 6 +- .../ConsumesAttributeEndpointRoutingTests.cs | 4 +- .../ConsumesAttributeTests.cs | 4 +- .../ContentNegotiationTest.cs | 4 +- .../DefaultValuesTest.cs | 4 +- .../FiltersTest.cs | 4 +- .../JsonResultTest.cs | 4 +- .../LinkGenerationTests.cs | 4 +- .../OutputFormatterTest.cs | 4 +- .../RazorPageModelTest.cs | 6 +- .../RazorPagesNamespaceTest.cs | 6 +- .../RazorPagesTest.cs | 6 +- .../RazorPagesViewSearchTest.cs | 6 +- .../RazorPagesWithEndpointRoutingTest.cs | 4 +- .../RemoteAttributeValidationTest.cs | 4 +- .../RequestServicesEndpointRoutingTest.cs | 4 +- .../RequestServicesTest.cs | 4 +- ...ngEndpointRoutingWithoutRazorPagesTests.cs | 4 +- .../RoutingUseMvcWithEndpointRoutingTest.cs | 386 +++++++++ .../RoutingWithoutRazorPagesTests.cs | 4 +- .../TempDataInCookiesTest.cs | 4 +- .../TempDataPropertyTest.cs | 4 +- .../TestingInfrastructureInheritanceTests.cs | 2 +- .../TestingInfrastructureTests.cs | 6 +- .../PageActionEndpointDataSourceTest.cs | 123 +++ ...soft.AspNetCore.Mvc.RazorPages.Test.csproj | 4 + .../WebSites/ApiExplorerWebSite/Startup.cs | 4 +- .../ApplicationModelWebSite/Startup.cs | 10 +- src/Mvc/test/WebSites/BasicWebSite/Program.cs | 2 +- src/Mvc/test/WebSites/BasicWebSite/Startup.cs | 65 +- .../BasicWebSite/StartupRequestLimitSize.cs | 6 +- ...hCookieTempDataProviderAndCookieConsent.cs | 6 +- ...artupWithCustomInvalidModelStateFactory.cs | 6 +- .../StartupWithEndpointRouting.cs | 49 -- .../StartupWithSessionTempDataProvider.cs | 7 +- .../StartupWithoutEndpointRouting.cs | 97 +++ .../ControllersFromServicesWebSite/Startup.cs | 4 +- src/Mvc/test/WebSites/CorsWebSite/Startup.cs | 7 +- .../StartupWithoutEndpointRouting.cs | 6 + .../ErrorPageMiddlewareWebSite/Startup.cs | 5 +- src/Mvc/test/WebSites/FilesWebSite/Startup.cs | 4 +- .../test/WebSites/FormatterWebSite/Startup.cs | 5 +- .../StartupWithRespectBrowserAcceptHeader.cs | 5 +- .../WebSites/GenericHostWebSite/Startup.cs | 9 +- .../WebSites/HtmlGenerationWebSite/Startup.cs | 13 +- .../StartupWithoutEndpointRouting.cs | 22 + .../WebSites/RazorBuildWebSite/Startup.cs | 6 +- .../WebSites/RazorPagesWebSite/Startup.cs | 34 +- .../RazorPagesWebSite/StartupWithBasePath.cs | 5 +- .../StartupWithEndpointRouting.cs | 36 - .../StartupWithoutEndpointRouting.cs | 55 ++ src/Mvc/test/WebSites/RazorWebSite/Startup.cs | 6 +- .../RazorWebSite/StartupDataAnnotations.cs | 8 +- .../test/WebSites/RoutingWebSite/Startup.cs | 51 +- .../RoutingWebSite/StartupForLinkGenerator.cs | 6 +- .../StartupWithUseMvcAndEndpointRouting.cs | 80 ++ .../StartupWithoutEndpointRouting.cs | 64 +- .../test/WebSites/SecurityWebSite/Startup.cs | 6 +- .../StartupWithGlobalDenyAnonymousFilter.cs | 5 +- .../test/WebSites/SimpleWebSite/Startup.cs | 5 +- .../WebSites/TagHelpersWebSite/Startup.cs | 5 +- .../WebSites/VersioningWebSite/Startup.cs | 7 +- .../StartupWithoutEndpointRouting.cs | 6 + .../WebSites/XmlFormattersWebSite/Startup.cs | 5 +- .../content/RazorPagesWeb-CSharp/Startup.cs | 2 +- .../content/StarterWeb-CSharp/Startup.cs | 2 +- .../content/StarterWeb-FSharp/Startup.fs | 2 +- .../content/WebApi-CSharp/Startup.cs | 2 +- .../content/WebApi-FSharp/Startup.fs | 2 +- 96 files changed, 2959 insertions(+), 1799 deletions(-) rename src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/{MvcEndpointDatasourceBenchmark.cs => ActionEndpointDatasourceBenchmark.cs} (82%) create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/ControllerEndpointRouteBuilderExtensions.cs delete mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs delete mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs delete mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSource.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSourceBase.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointFactory.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ControllerActionEndpointDataSource.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ConventionalRouteEntry.cs delete mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionEndpointDataSource.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceBaseTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointFactoryTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerActionEndpointDataSourceTest.cs delete mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/MvcEndpointDataSourceTests.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingUseMvcWithEndpointRoutingTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionEndpointDataSourceTest.cs delete mode 100644 src/Mvc/test/WebSites/BasicWebSite/StartupWithEndpointRouting.cs create mode 100644 src/Mvc/test/WebSites/BasicWebSite/StartupWithoutEndpointRouting.cs delete mode 100644 src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithEndpointRouting.cs create mode 100644 src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs create mode 100644 src/Mvc/test/WebSites/RoutingWebSite/StartupWithUseMvcAndEndpointRouting.cs diff --git a/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs b/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionEndpointDatasourceBenchmark.cs similarity index 82% rename from src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs rename to src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionEndpointDatasourceBenchmark.cs index 93e78e9094..2d5b951f01 100644 --- a/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs +++ b/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionEndpointDatasourceBenchmark.cs @@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Mvc.Performance { - public class MvcEndpointDataSourceBenchmark + public class ActionEndpointDataSourceBenchmark { private const string DefaultRoute = "{Controller=Home}/{Action=Index}/{id?}"; @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Performance private MockActionDescriptorCollectionProvider _conventionalActionProvider; private MockActionDescriptorCollectionProvider _attributeActionProvider; - private List _conventionalEndpointInfos; + private List _routes; [Params(1, 100, 1000)] public int ActionCount; @@ -41,22 +41,21 @@ namespace Microsoft.AspNetCore.Mvc.Performance Enumerable.Range(0, ActionCount).Select(i => CreateAttributeRoutedAction(i)).ToList() ); - _conventionalEndpointInfos = new List + _routes = new List { - new MvcEndpointInfo( + new ConventionalRouteEntry( "Default", DefaultRoute, new RouteValueDictionary(), new Dictionary(), - new RouteValueDictionary(), - new MockParameterPolicyFactory()) + new RouteValueDictionary()) }; } [Benchmark] public void AttributeRouteEndpoints() { - var endpointDataSource = CreateMvcEndpointDataSource(_attributeActionProvider); + var endpointDataSource = CreateDataSource(_attributeActionProvider); var endpoints = endpointDataSource.Endpoints; AssertHasEndpoints(endpoints); @@ -65,10 +64,13 @@ namespace Microsoft.AspNetCore.Mvc.Performance [Benchmark] public void ConventionalEndpoints() { - var endpointDataSource = CreateMvcEndpointDataSource(_conventionalActionProvider); - endpointDataSource.ConventionalEndpointInfos.AddRange(_conventionalEndpointInfos); - var endpoints = endpointDataSource.Endpoints; + var dataSource = CreateDataSource(_conventionalActionProvider); + for (var i = 0; i < _routes.Count; i++) + { + dataSource.AddRoute(_routes[i]); + } + var endpoints = dataSource.Endpoints; AssertHasEndpoints(endpoints); } @@ -108,14 +110,14 @@ namespace Microsoft.AspNetCore.Mvc.Performance }; } - private MvcEndpointDataSource CreateMvcEndpointDataSource( - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) + private ActionEndpointDataSource CreateDataSource(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) { - var dataSource = new MvcEndpointDataSource( + var dataSource = new ActionEndpointDataSource( actionDescriptorCollectionProvider, - new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), - new MockParameterPolicyFactory(), - new MockRoutePatternTransformer()); + new ActionEndpointFactory( + new MockRoutePatternTransformer(), + new MvcEndpointInvokerFactory( + new ActionInvokerFactory(Array.Empty())))); return dataSource; } diff --git a/src/Mvc/samples/MvcSandbox/Startup.cs b/src/Mvc/samples/MvcSandbox/Startup.cs index d6c039de1a..bb6257bf65 100644 --- a/src/Mvc/samples/MvcSandbox/Startup.cs +++ b/src/Mvc/samples/MvcSandbox/Startup.cs @@ -66,7 +66,8 @@ namespace MvcSandbox return Task.CompletedTask; }); - builder.MapApplication(); + builder.MapControllers(); + builder.MapRazorPages(); }); app.UseDeveloperExceptionPage(); diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/ControllerEndpointRouteBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/ControllerEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..6d96bde95e --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/ControllerEndpointRouteBuilderExtensions.cs @@ -0,0 +1,179 @@ +// 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.Linq; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Contains extension methods for using Controllers with . + /// + public static class ControllerEndpointRouteBuilderExtensions + { + /// + /// Adds endpoints for controller actions to the without specifying any routes. + /// + /// The . + /// An for endpoints associated with controller actions. + public static IEndpointConventionBuilder MapControllers(this IEndpointRouteBuilder routes) + { + if (routes == null) + { + throw new ArgumentNullException(nameof(routes)); + } + + EnsureControllerServices(routes); + + return GetOrCreateDataSource(routes); + } + + /// + /// Adds endpoints for controller actions to the and adds the default route + /// {controller=Home}/{action=Index}/{id?}. + /// + /// The . + /// An for endpoints associated with controller actions. + public static IEndpointConventionBuilder MapDefaultControllerRoute(this IEndpointRouteBuilder routes) + { + if (routes == null) + { + throw new ArgumentNullException(nameof(routes)); + } + + EnsureControllerServices(routes); + + var dataSource = GetOrCreateDataSource(routes); + dataSource.AddRoute(new ConventionalRouteEntry( + "default", + "{controller=Home}/{action=Index}/{id?}", + defaults: null, + constraints: null, + dataTokens: null)); + + return dataSource; + } + + /// + /// Adds endpoints for controller actions to the and specifies a route + /// with the given , , + /// , , and . + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the + /// names and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and + /// values of the constraints. + /// + /// + /// An object that contains data tokens for the route. The object's properties represent the names and + /// values of the data tokens. + /// + public static void MapControllerRoute( + this IEndpointRouteBuilder routes, + string name, + string template, + object defaults = null, + object constraints = null, + object dataTokens = null) + { + if (routes == null) + { + throw new ArgumentNullException(nameof(routes)); + } + + EnsureControllerServices(routes); + + var dataSource = GetOrCreateDataSource(routes); + dataSource.AddRoute(new ConventionalRouteEntry( + name, + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens))); + } + + /// + /// Adds endpoints for controller actions to the and specifies a route + /// with the given , , , + /// , , and . + /// + /// The to add the route to. + /// The name of the route. + /// The area name. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the + /// names and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and + /// values of the constraints. + /// + /// + /// An object that contains data tokens for the route. The object's properties represent the names and + /// values of the data tokens. + /// + public static void MapAreaControllerRoute( + this IEndpointRouteBuilder routes, + string name, + string areaName, + string template, + object defaults = null, + object constraints = null, + object dataTokens = null) + { + if (routes == null) + { + throw new ArgumentNullException(nameof(routes)); + } + + if (string.IsNullOrEmpty(areaName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName)); + } + + var defaultsDictionary = new RouteValueDictionary(defaults); + defaultsDictionary["area"] = defaultsDictionary["area"] ?? areaName; + + var constraintsDictionary = new RouteValueDictionary(constraints); + constraintsDictionary["area"] = constraintsDictionary["area"] ?? new StringRouteConstraint(areaName); + + routes.MapControllerRoute(name, template, defaultsDictionary, constraintsDictionary, dataTokens); + } + + private static void EnsureControllerServices(IEndpointRouteBuilder routes) + { + var marker = routes.ServiceProvider.GetService(); + if (marker == null) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + "AddMvc", + "ConfigureServices(...)")); + } + } + + private static ControllerActionEndpointDataSource GetOrCreateDataSource(IEndpointRouteBuilder routes) + { + var dataSource = routes.DataSources.OfType().FirstOrDefault(); + if (dataSource == null) + { + dataSource = routes.ServiceProvider.GetRequiredService(); + routes.DataSources.Add(dataSource); + } + + return dataSource; + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs deleted file mode 100644 index 6691d83d8f..0000000000 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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 Microsoft.AspNetCore.Routing; - -namespace Microsoft.AspNetCore.Builder -{ - internal class DefaultEndpointConventionBuilder : IEndpointConventionBuilder - { - public DefaultEndpointConventionBuilder() - { - Conventions = new List>(); - } - - public List> Conventions { get; } - - public void Add(Action convention) - { - Conventions.Add(convention); - } - } -} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs index 6055f491a1..bb1903fefc 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs @@ -88,10 +88,7 @@ namespace Microsoft.AspNetCore.Builder if (options.Value.EnableEndpointRouting) { - var mvcEndpointDataSource = app.ApplicationServices - .GetRequiredService(); - var parameterPolicyFactory = app.ApplicationServices - .GetRequiredService(); + var dataSource = app.ApplicationServices.GetRequiredService(); var endpointRouteBuilder = new EndpointRouteBuilder(app); @@ -103,15 +100,14 @@ namespace Microsoft.AspNetCore.Builder // Sub-types could have additional customization that we can't knowingly convert if (router is Route route && router.GetType() == typeof(Route)) { - var endpointInfo = new MvcEndpointInfo( + var entry = new ConventionalRouteEntry( route.Name, route.RouteTemplate, route.Defaults, route.Constraints.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value), - route.DataTokens, - parameterPolicyFactory); + route.DataTokens); - mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo); + dataSource.AddRoute(entry); } else { @@ -119,20 +115,13 @@ namespace Microsoft.AspNetCore.Builder } } - // Include all controllers with attribute routing and Razor pages - var defaultEndpointConventionBuilder = new DefaultEndpointConventionBuilder(); - mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add((actionDescriptor) => - { - return defaultEndpointConventionBuilder; - }); - if (!app.Properties.TryGetValue(EndpointRoutingRegisteredKey, out _)) { // Matching middleware has not been registered yet // For back-compat register middleware so an endpoint is matched and then immediately used app.UseRouting(routerBuilder => { - routerBuilder.DataSources.Add(mvcEndpointDataSource); + routerBuilder.DataSources.Add(dataSource); }); } diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs deleted file mode 100644 index 5712fa6a55..0000000000 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs +++ /dev/null @@ -1,79 +0,0 @@ -// 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.Globalization; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Patterns; - -namespace Microsoft.AspNetCore.Builder -{ - internal class MvcEndpointInfo : DefaultEndpointConventionBuilder - { - public MvcEndpointInfo( - string name, - string pattern, - RouteValueDictionary defaults, - IDictionary constraints, - RouteValueDictionary dataTokens, - ParameterPolicyFactory parameterPolicyFactory) - { - Name = name; - Pattern = pattern ?? string.Empty; - DataTokens = dataTokens; - - try - { - // Data we parse from the pattern will be used to fill in the rest of the constraints or - // defaults. The parser will throw for invalid routes. - ParsedPattern = RoutePatternFactory.Parse(pattern, defaults, constraints); - ParameterPolicies = BuildParameterPolicies(ParsedPattern.Parameters, parameterPolicyFactory); - - Defaults = defaults; - // Merge defaults outside of RoutePattern because the defaults will already have values from pattern - MergedDefaults = new RouteValueDictionary(ParsedPattern.Defaults); - } - catch (Exception exception) - { - throw new RouteCreationException( - string.Format(CultureInfo.CurrentCulture, "An error occurred while creating the route with name '{0}' and pattern '{1}'.", name, pattern), exception); - } - } - - public string Name { get; } - public string Pattern { get; } - - // Non-inline defaults - public RouteValueDictionary Defaults { get; } - - // Inline and non-inline defaults merged into one - public RouteValueDictionary MergedDefaults { get; } - - public IDictionary> ParameterPolicies { get; } - public RouteValueDictionary DataTokens { get; } - public RoutePattern ParsedPattern { get; private set; } - - internal static Dictionary> BuildParameterPolicies(IReadOnlyList parameters, ParameterPolicyFactory parameterPolicyFactory) - { - var policies = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - foreach (var parameter in parameters) - { - foreach (var parameterPolicy in parameter.ParameterPolicies) - { - var createdPolicy = parameterPolicyFactory.Create(parameter, parameterPolicy); - if (!policies.TryGetValue(parameter.Name, out var policyList)) - { - policyList = new List(); - policies.Add(parameter.Name, policyList); - } - - policyList.Add(createdPolicy); - } - } - - return policies; - } - } -} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs deleted file mode 100644 index bef1b53372..0000000000 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -// 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.Linq; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Builder -{ - public static class MvcEndpointRouteBuilderExtensions - { - public static IEndpointConventionBuilder MapApplication( - this IEndpointRouteBuilder routeBuilder) - { - return MapActionDescriptors(routeBuilder, null); - } - - public static IEndpointConventionBuilder MapAssembly( - this IEndpointRouteBuilder routeBuilder) - { - return MapActionDescriptors(routeBuilder, typeof(TContainingType)); - } - - private static IEndpointConventionBuilder MapActionDescriptors( - this IEndpointRouteBuilder routeBuilder, - Type containingType) - { - var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault(); - - if (mvcEndpointDataSource == null) - { - mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService(); - routeBuilder.DataSources.Add(mvcEndpointDataSource); - } - - var conventionBuilder = new DefaultEndpointConventionBuilder(); - - var assemblyFilter = containingType?.Assembly; - - mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add(actionDescriptor => - { - // Filter a descriptor by the assembly - // Note that this will only filter actions on controllers - // Does not support filtering Razor pages embedded in assemblies - if (assemblyFilter != null) - { - if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor) - { - if (controllerActionDescriptor.ControllerTypeInfo.Assembly != assemblyFilter) - { - return null; - } - } - } - - return conventionBuilder; - }); - - return conventionBuilder; - } - - public static IEndpointConventionBuilder MapControllerRoute( - this IEndpointRouteBuilder routeBuilder, - string name, - string template) - { - return MapControllerRoute(routeBuilder, name, template, defaults: null); - } - - public static IEndpointConventionBuilder MapControllerRoute( - this IEndpointRouteBuilder routeBuilder, - string name, - string template, - object defaults) - { - return MapControllerRoute(routeBuilder, name, template, defaults, constraints: null); - } - - public static IEndpointConventionBuilder MapControllerRoute( - this IEndpointRouteBuilder routeBuilder, - string name, - string template, - object defaults, - object constraints) - { - return MapControllerRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null); - } - - public static IEndpointConventionBuilder MapControllerRoute( - this IEndpointRouteBuilder routeBuilder, - string name, - string template, - object defaults, - object constraints, - object dataTokens) - { - var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault(); - - if (mvcEndpointDataSource == null) - { - mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService(); - routeBuilder.DataSources.Add(mvcEndpointDataSource); - } - - var endpointInfo = new MvcEndpointInfo( - name, - template, - new RouteValueDictionary(defaults), - new RouteValueDictionary(constraints), - new RouteValueDictionary(dataTokens), - routeBuilder.ServiceProvider.GetRequiredService()); - - mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo); - - return endpointInfo; - } - } -} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index c43eb07243..ca26f6740e 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -269,7 +269,9 @@ namespace Microsoft.Extensions.DependencyInjection // // Endpoint Routing / Endpoints // - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); // diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSource.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSource.cs new file mode 100644 index 0000000000..8132177195 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSource.cs @@ -0,0 +1,65 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + // This is only used to support the scenario where UseMvc is called with + // EnableEndpointRouting = true. For layering reasons we can't use the PageActionEndpointDataSource + // here. + internal class ActionEndpointDataSource : ActionEndpointDataSourceBase + { + private readonly ActionEndpointFactory _endpointFactory; + private readonly List _routes; + + public ActionEndpointDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) + : base(actions) + { + _endpointFactory = endpointFactory; + + _routes = new List(); + + // IMPORTANT: this needs to be the last thing we do in the constructor. + // Change notifications can happen immediately! + Subscribe(); + } + + // For testing + public IReadOnlyList Routes + { + get + { + lock (Lock) + { + return _routes.ToArray(); + } + } + } + + public void AddRoute(in ConventionalRouteEntry route) + { + lock (Lock) + { + _routes.Add(route); + } + } + + protected override List CreateEndpoints(IReadOnlyList actions, IReadOnlyList> conventions) + { + var endpoints = new List(); + for (var i = 0; i < actions.Count; i++) + { + _endpointFactory.AddEndpoints(endpoints, actions[i], _routes, conventions); + } + + return endpoints; + } + } +} + diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSourceBase.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSourceBase.cs new file mode 100644 index 0000000000..557df014b0 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointDataSourceBase.cs @@ -0,0 +1,136 @@ +// 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.Diagnostics; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + internal abstract class ActionEndpointDataSourceBase : EndpointDataSource, IEndpointConventionBuilder, IDisposable + { + private readonly IActionDescriptorCollectionProvider _actions; + + // The following are protected by this lock for WRITES only. This pattern is similar + // to DefaultActionDescriptorChangeProvider - see comments there for details on + // all of the threading behaviors. + protected readonly object Lock = new object(); + + private List _endpoints; + private CancellationTokenSource _cancellationTokenSource; + private IChangeToken _changeToken; + private IDisposable _disposable; + + // Protected for READS and WRITES. + private readonly List> _conventions; + + public ActionEndpointDataSourceBase(IActionDescriptorCollectionProvider actions) + { + _actions = actions; + + _conventions = new List>(); + } + + public override IReadOnlyList Endpoints + { + get + { + Initialize(); + Debug.Assert(_changeToken != null); + Debug.Assert(_endpoints != null); + return _endpoints; + } + } + + // Will be called with the lock. + protected abstract List CreateEndpoints(IReadOnlyList actions, IReadOnlyList> conventions); + + protected void Subscribe() + { + // IMPORTANT: this needs to be called by the derived class to avoid the fragile base class + // problem. We can't call this in the base-class constuctor because it's too early. + // + // It's possible for someone to override the collection provider without providing + // change notifications. If that's the case we won't process changes. + if (_actions is ActionDescriptorCollectionProvider collectionProviderWithChangeToken) + { + _disposable = ChangeToken.OnChange( + () => collectionProviderWithChangeToken.GetChangeToken(), + UpdateEndpoints); + } + } + + public void Add(Action convention) + { + if (convention == null) + { + throw new ArgumentNullException(nameof(convention)); + } + + lock (Lock) + { + _conventions.Add(convention); + } + } + + public override IChangeToken GetChangeToken() + { + Initialize(); + Debug.Assert(_changeToken != null); + Debug.Assert(_endpoints != null); + return _changeToken; + } + + public void Dispose() + { + // Once disposed we won't process updates anymore, but we still allow access to the endpoints. + _disposable?.Dispose(); + _disposable = null; + } + + private void Initialize() + { + if (_endpoints == null) + { + lock (Lock) + { + if (_endpoints == null) + { + UpdateEndpoints(); + } + } + } + } + + private void UpdateEndpoints() + { + lock (Lock) + { + var endpoints = CreateEndpoints(_actions.ActionDescriptors.Items, _conventions); + + // See comments in DefaultActionDescriptorCollectionProvider. These steps are done + // in a specific order to ensure callers always see a consistent state. + + // Step 1 - capture old token + var oldCancellationTokenSource = _cancellationTokenSource; + + // Step 2 - update endpoints + _endpoints = endpoints; + + // Step 3 - create new change token + _cancellationTokenSource = new CancellationTokenSource(); + _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); + + // Step 4 - trigger old token + oldCancellationTokenSource?.Cancel(); + } + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointFactory.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointFactory.cs new file mode 100644 index 0000000000..d1392c80b3 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionEndpointFactory.cs @@ -0,0 +1,278 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + internal class ActionEndpointFactory + { + private readonly RoutePatternTransformer _routePatternTransformer; + private readonly MvcEndpointInvokerFactory _invokerFactory; + + public ActionEndpointFactory( + RoutePatternTransformer routePatternTransformer, + MvcEndpointInvokerFactory invokerFactory) + { + if (routePatternTransformer == null) + { + throw new ArgumentNullException(nameof(routePatternTransformer)); + } + + if (invokerFactory == null) + { + throw new ArgumentNullException(nameof(invokerFactory)); + } + + _routePatternTransformer = routePatternTransformer; + _invokerFactory = invokerFactory; + } + + public void AddEndpoints( + List endpoints, + ActionDescriptor action, + IReadOnlyList routes, + IReadOnlyList> conventions) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (routes == null) + { + throw new ArgumentNullException(nameof(routes)); + } + + if (conventions == null) + { + throw new ArgumentNullException(nameof(conventions)); + } + + if (action.AttributeRouteInfo == null) + { + // In traditional conventional routing setup, the routes defined by a user have a static order + // defined by how they are added into the list. We would like to maintain the same order when building + // up the endpoints too. + // + // Start with an order of '1' for conventional routes as attribute routes have a default order of '0'. + // This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world. + var conventionalRouteOrder = 1; + + // Check each of the conventional patterns to see if the action would be reachable. + // If the action and pattern are compatible then create an endpoint with action + // route values on the pattern. + foreach (var route in routes) + { + // A route is applicable if: + // 1. It has a parameter (or default value) for 'required' non-null route value + // 2. It does not have a parameter (or default value) for 'required' null route value + var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(route.Pattern, action.RouteValues); + if (updatedRoutePattern == null) + { + continue; + } + + var builder = CreateEndpoint( + action, + updatedRoutePattern, + route.RouteName, + conventionalRouteOrder++, + route.DataTokens, + suppressLinkGeneration: false, + suppressPathMatching: false, + conventions); + endpoints.Add(builder); + } + } + else + { + var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template); + + // Modify the route and required values to ensure required values can be successfully subsituted. + // Subsitituting required values into an attribute route pattern should always succeed. + var (resolvedRoutePattern, resolvedRouteValues) = ResolveDefaultsAndRequiredValues(action, attributeRoutePattern); + + var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(resolvedRoutePattern, resolvedRouteValues); + if (updatedRoutePattern == null) + { + throw new InvalidOperationException("Failed to update route pattern with required values."); + } + + var endpoint = CreateEndpoint( + action, + updatedRoutePattern, + action.AttributeRouteInfo.Name, + action.AttributeRouteInfo.Order, + dataTokens: null, + action.AttributeRouteInfo.SuppressLinkGeneration, + action.AttributeRouteInfo.SuppressPathMatching, + conventions); + endpoints.Add(endpoint); + } + } + + private static (RoutePattern resolvedRoutePattern, IDictionary resolvedRequiredValues) ResolveDefaultsAndRequiredValues(ActionDescriptor action, RoutePattern attributeRoutePattern) + { + RouteValueDictionary updatedDefaults = null; + IDictionary resolvedRequiredValues = null; + + foreach (var routeValue in action.RouteValues) + { + var parameter = attributeRoutePattern.GetParameter(routeValue.Key); + + if (!RouteValueEqualityComparer.Default.Equals(routeValue.Value, string.Empty)) + { + if (parameter == null) + { + // The attribute route has a required value with no matching parameter + // Add the required values without a parameter as a default + // e.g. + // Template: "Login/{action}" + // Required values: { controller = "Login", action = "Index" } + // Updated defaults: { controller = "Login" } + + if (updatedDefaults == null) + { + updatedDefaults = new RouteValueDictionary(attributeRoutePattern.Defaults); + } + + updatedDefaults[routeValue.Key] = routeValue.Value; + } + } + else + { + if (parameter != null) + { + // The attribute route has a null or empty required value with a matching parameter + // Remove the required value from the route + + if (resolvedRequiredValues == null) + { + resolvedRequiredValues = new Dictionary(action.RouteValues); + } + + resolvedRequiredValues.Remove(parameter.Name); + } + } + } + if (updatedDefaults != null) + { + attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template, updatedDefaults, parameterPolicies: null); + } + + return (attributeRoutePattern, resolvedRequiredValues ?? action.RouteValues); + } + + private RouteEndpoint CreateEndpoint( + ActionDescriptor action, + RoutePattern routePattern, + string routeName, + int order, + RouteValueDictionary dataTokens, + bool suppressLinkGeneration, + bool suppressPathMatching, + IReadOnlyList> conventions) + { + RequestDelegate requestDelegate = (context) => + { + var routeData = context.GetRouteData(); + + var actionContext = new ActionContext(context, routeData, action); + + var invoker = _invokerFactory.CreateInvoker(actionContext); + return invoker.InvokeAsync(); + }; + + var builder = new RouteEndpointBuilder(requestDelegate, routePattern, order) + { + DisplayName = action.DisplayName, + }; + + // Add action metadata first so it has a low precedence + if (action.EndpointMetadata != null) + { + foreach (var d in action.EndpointMetadata) + { + builder.Metadata.Add(d); + } + } + + builder.Metadata.Add(action); + + if (dataTokens != null) + { + builder.Metadata.Add(new DataTokensMetadata(dataTokens)); + } + + builder.Metadata.Add(new RouteNameMetadata(routeName)); + + // Add filter descriptors to endpoint metadata + if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0) + { + foreach (var filter in action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer).Select(f => f.Filter)) + { + builder.Metadata.Add(filter); + } + } + + if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) + { + // We explicitly convert a few types of action constraints into MatcherPolicy+Metadata + // to better integrate with the DFA matcher. + // + // Other IActionConstraint data will trigger a back-compat path that can execute + // action constraints. + foreach (var actionConstraint in action.ActionConstraints) + { + if (actionConstraint is HttpMethodActionConstraint httpMethodActionConstraint && + !builder.Metadata.OfType().Any()) + { + builder.Metadata.Add(new HttpMethodMetadata(httpMethodActionConstraint.HttpMethods)); + } + else if (actionConstraint is ConsumesAttribute consumesAttribute && + !builder.Metadata.OfType().Any()) + { + builder.Metadata.Add(new ConsumesMetadata(consumesAttribute.ContentTypes.ToArray())); + } + else if (!builder.Metadata.Contains(actionConstraint)) + { + // The constraint might have been added earlier, e.g. it is also a filter descriptor + builder.Metadata.Add(actionConstraint); + } + } + } + + if (suppressLinkGeneration) + { + builder.Metadata.Add(new SuppressLinkGenerationMetadata()); + } + + if (suppressPathMatching) + { + builder.Metadata.Add(new SuppressMatchingMetadata()); + } + + for (var i = 0; i < conventions.Count; i++) + { + conventions[i](builder); + } + + return (RouteEndpoint)builder.Build(); + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ControllerActionEndpointDataSource.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ControllerActionEndpointDataSource.cs new file mode 100644 index 0000000000..9a1ce02cd8 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ControllerActionEndpointDataSource.cs @@ -0,0 +1,54 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + internal class ControllerActionEndpointDataSource : ActionEndpointDataSourceBase + { + private readonly ActionEndpointFactory _endpointFactory; + private readonly List _routes; + + public ControllerActionEndpointDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) + : base(actions) + { + _endpointFactory = endpointFactory; + + _routes = new List(); + + // IMPORTANT: this needs to be the last thing we do in the constructor. + // Change notifications can happen immediately! + Subscribe(); + } + + public void AddRoute(in ConventionalRouteEntry route) + { + lock (Lock) + { + _routes.Add(route); + } + } + + protected override List CreateEndpoints(IReadOnlyList actions, IReadOnlyList> conventions) + { + var endpoints = new List(); + for (var i = 0; i < actions.Count; i++) + { + if (actions[i] is ControllerActionDescriptor action) + { + _endpointFactory.AddEndpoints(endpoints, action, _routes, conventions); + } + } + + return endpoints; + } + } +} + diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ConventionalRouteEntry.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ConventionalRouteEntry.cs new file mode 100644 index 0000000000..18d587daac --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ConventionalRouteEntry.cs @@ -0,0 +1,56 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + internal readonly struct ConventionalRouteEntry + { + public readonly RoutePattern Pattern; + public readonly string RouteName; + public readonly RouteValueDictionary DataTokens; + + public ConventionalRouteEntry( + string routeName, + string pattern, + RouteValueDictionary defaults, + IDictionary constraints, + RouteValueDictionary dataTokens) + { + RouteName = routeName; + DataTokens = dataTokens; + + try + { + // Data we parse from the pattern will be used to fill in the rest of the constraints or + // defaults. The parser will throw for invalid routes. + Pattern = RoutePatternFactory.Parse(pattern, defaults, constraints); + } + catch (Exception exception) + { + throw new RouteCreationException(string.Format( + CultureInfo.CurrentCulture, + "An error occurred while creating the route with name '{0}' and pattern '{1}'.", + routeName, + pattern), exception); + } + } + + public ConventionalRouteEntry(RoutePattern pattern, string routeName, RouteValueDictionary dataTokens) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + Pattern = pattern; + RouteName = routeName; + DataTokens = dataTokens; + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs deleted file mode 100644 index 40e55ae0a8..0000000000 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs +++ /dev/null @@ -1,381 +0,0 @@ -// 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.Diagnostics; -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ActionConstraints; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Mvc.Routing -{ - internal class MvcEndpointDataSource : EndpointDataSource - { - private readonly IActionDescriptorCollectionProvider _actions; - private readonly MvcEndpointInvokerFactory _invokerFactory; - private readonly ParameterPolicyFactory _parameterPolicyFactory; - private readonly RoutePatternTransformer _routePatternTransformer; - - // The following are protected by this lock for WRITES only. This pattern is similar - // to DefaultActionDescriptorChangeProvider - see comments there for details on - // all of the threading behaviors. - private readonly object _lock = new object(); - private List _endpoints; - private CancellationTokenSource _cancellationTokenSource; - private IChangeToken _changeToken; - - public MvcEndpointDataSource( - IActionDescriptorCollectionProvider actions, - MvcEndpointInvokerFactory invokerFactory, - ParameterPolicyFactory parameterPolicyFactory, - RoutePatternTransformer routePatternTransformer) - { - _actions = actions; - _invokerFactory = invokerFactory; - _parameterPolicyFactory = parameterPolicyFactory; - _routePatternTransformer = routePatternTransformer; - - ConventionalEndpointInfos = new List(); - AttributeRoutingConventionResolvers = new List>(); - - // IMPORTANT: this needs to be the last thing we do in the constructor. Change notifications can happen immediately! - // - // It's possible for someone to override the collection provider without providing - // change notifications. If that's the case we won't process changes. - if (actions is ActionDescriptorCollectionProvider collectionProviderWithChangeToken) - { - ChangeToken.OnChange( - () => collectionProviderWithChangeToken.GetChangeToken(), - UpdateEndpoints); - } - } - - public List ConventionalEndpointInfos { get; } - - public List> AttributeRoutingConventionResolvers { get; } - - public override IReadOnlyList Endpoints - { - get - { - Initialize(); - Debug.Assert(_changeToken != null); - Debug.Assert(_endpoints != null); - return _endpoints; - } - } - - public override IChangeToken GetChangeToken() - { - Initialize(); - Debug.Assert(_changeToken != null); - Debug.Assert(_endpoints != null); - return _changeToken; - } - - private void Initialize() - { - if (_endpoints == null) - { - lock (_lock) - { - if (_endpoints == null) - { - UpdateEndpoints(); - } - } - } - } - - private void UpdateEndpoints() - { - lock (_lock) - { - var endpoints = new List(); - - foreach (var action in _actions.ActionDescriptors.Items) - { - if (action.AttributeRouteInfo == null) - { - // In traditional conventional routing setup, the routes defined by a user have a static order - // defined by how they are added into the list. We would like to maintain the same order when building - // up the endpoints too. - // - // Start with an order of '1' for conventional routes as attribute routes have a default order of '0'. - // This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world. - var conventionalRouteOrder = 1; - - // Check each of the conventional patterns to see if the action would be reachable. - // If the action and pattern are compatible then create an endpoint with action - // route values on the pattern. - foreach (var endpointInfo in ConventionalEndpointInfos) - { - // An 'endpointInfo' is applicable if: - // 1. It has a parameter (or default value) for 'required' non-null route value - // 2. It does not have a parameter (or default value) for 'required' null route value - var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(endpointInfo.ParsedPattern, action.RouteValues); - - if (updatedRoutePattern == null) - { - continue; - } - - var endpoint = CreateEndpoint( - action, - updatedRoutePattern, - endpointInfo.Name, - conventionalRouteOrder++, - endpointInfo.DataTokens, - false, - false, - endpointInfo.Conventions); - endpoints.Add(endpoint); - } - } - else - { - var conventionBuilder = ResolveActionConventionBuilder(action); - if (conventionBuilder == null) - { - // No convention builder for this action - // Do not create an endpoint for it - continue; - } - - var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template); - - // Modify the route and required values to ensure required values can be successfully subsituted. - // Subsitituting required values into an attribute route pattern should always succeed. - var (resolvedRoutePattern, resolvedRouteValues) = ResolveDefaultsAndRequiredValues(action, attributeRoutePattern); - - var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(resolvedRoutePattern, resolvedRouteValues); - - if (updatedRoutePattern == null) - { - throw new InvalidOperationException("Failed to update route pattern with required values."); - } - - var endpoint = CreateEndpoint( - action, - updatedRoutePattern, - action.AttributeRouteInfo.Name, - action.AttributeRouteInfo.Order, - dataTokens: null, - action.AttributeRouteInfo.SuppressLinkGeneration, - action.AttributeRouteInfo.SuppressPathMatching, - conventionBuilder.Conventions); - endpoints.Add(endpoint); - } - } - - // See comments in DefaultActionDescriptorCollectionProvider. These steps are done - // in a specific order to ensure callers always see a consistent state. - - // Step 1 - capture old token - var oldCancellationTokenSource = _cancellationTokenSource; - - // Step 2 - update endpoints - _endpoints = endpoints; - - // Step 3 - create new change token - _cancellationTokenSource = new CancellationTokenSource(); - _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); - - // Step 4 - trigger old token - oldCancellationTokenSource?.Cancel(); - } - } - - private static (RoutePattern resolvedRoutePattern, IDictionary resolvedRequiredValues) ResolveDefaultsAndRequiredValues(ActionDescriptor action, RoutePattern attributeRoutePattern) - { - RouteValueDictionary updatedDefaults = null; - IDictionary resolvedRequiredValues = null; - - foreach (var routeValue in action.RouteValues) - { - var parameter = attributeRoutePattern.GetParameter(routeValue.Key); - - if (!RouteValueEqualityComparer.Default.Equals(routeValue.Value, string.Empty)) - { - if (parameter == null) - { - // The attribute route has a required value with no matching parameter - // Add the required values without a parameter as a default - // e.g. - // Template: "Login/{action}" - // Required values: { controller = "Login", action = "Index" } - // Updated defaults: { controller = "Login" } - - if (updatedDefaults == null) - { - updatedDefaults = new RouteValueDictionary(attributeRoutePattern.Defaults); - } - - updatedDefaults[routeValue.Key] = routeValue.Value; - } - } - else - { - if (parameter != null) - { - // The attribute route has a null or empty required value with a matching parameter - // Remove the required value from the route - - if (resolvedRequiredValues == null) - { - resolvedRequiredValues = new Dictionary(action.RouteValues); - } - - resolvedRequiredValues.Remove(parameter.Name); - } - } - } - if (updatedDefaults != null) - { - attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template, updatedDefaults, parameterPolicies: null); - } - - return (attributeRoutePattern, resolvedRequiredValues ?? action.RouteValues); - } - - private DefaultEndpointConventionBuilder ResolveActionConventionBuilder(ActionDescriptor action) - { - foreach (var filter in AttributeRoutingConventionResolvers) - { - var conventionBuilder = filter(action); - if (conventionBuilder != null) - { - return conventionBuilder; - } - } - - return null; - } - - private RouteEndpoint CreateEndpoint( - ActionDescriptor action, - RoutePattern routePattern, - string routeName, - int order, - RouteValueDictionary dataTokens, - bool suppressLinkGeneration, - bool suppressPathMatching, - List> conventions) - { - RequestDelegate requestDelegate = (context) => - { - var routeData = context.GetRouteData(); - - var actionContext = new ActionContext(context, routeData, action); - - var invoker = _invokerFactory.CreateInvoker(actionContext); - return invoker.InvokeAsync(); - }; - - var endpointBuilder = new RouteEndpointBuilder(requestDelegate, routePattern, order); - AddEndpointMetadata( - endpointBuilder.Metadata, - action, - routeName, - dataTokens, - suppressLinkGeneration, - suppressPathMatching); - - endpointBuilder.DisplayName = action.DisplayName; - - // REVIEW: When should conventions be run - // Metadata should have lower precedence that data source metadata - if (conventions != null) - { - foreach (var convention in conventions) - { - convention(endpointBuilder); - } - } - - return (RouteEndpoint)endpointBuilder.Build(); - } - - private static void AddEndpointMetadata( - IList metadata, - ActionDescriptor action, - string routeName, - RouteValueDictionary dataTokens, - bool suppressLinkGeneration, - bool suppressPathMatching) - { - // Add action metadata first so it has a low precedence - if (action.EndpointMetadata != null) - { - foreach (var d in action.EndpointMetadata) - { - metadata.Add(d); - } - } - - metadata.Add(action); - - if (dataTokens != null) - { - metadata.Add(new DataTokensMetadata(dataTokens)); - } - - metadata.Add(new RouteNameMetadata(routeName)); - - // Add filter descriptors to endpoint metadata - if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0) - { - foreach (var filter in action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer).Select(f => f.Filter)) - { - metadata.Add(filter); - } - } - - if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) - { - // We explicitly convert a few types of action constraints into MatcherPolicy+Metadata - // to better integrate with the DFA matcher. - // - // Other IActionConstraint data will trigger a back-compat path that can execute - // action constraints. - foreach (var actionConstraint in action.ActionConstraints) - { - if (actionConstraint is HttpMethodActionConstraint httpMethodActionConstraint && - !metadata.OfType().Any()) - { - metadata.Add(new HttpMethodMetadata(httpMethodActionConstraint.HttpMethods)); - } - else if (actionConstraint is ConsumesAttribute consumesAttribute && - !metadata.OfType().Any()) - { - metadata.Add(new ConsumesMetadata(consumesAttribute.ContentTypes.ToArray())); - } - else if (!metadata.Contains(actionConstraint)) - { - // The constraint might have been added earlier, e.g. it is also a filter descriptor - metadata.Add(actionConstraint); - } - } - } - - if (suppressLinkGeneration) - { - metadata.Add(new SuppressLinkGenerationMetadata()); - } - - if (suppressPathMatching) - { - metadata.Add(new SuppressMatchingMetadata()); - } - } - } -} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Builder/RazorPagesEndpointRouteBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Builder/RazorPagesEndpointRouteBuilderExtensions.cs index 677a426f34..444004e8e4 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Builder/RazorPagesEndpointRouteBuilderExtensions.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Builder/RazorPagesEndpointRouteBuilderExtensions.cs @@ -1,9 +1,9 @@ // 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.Linq; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -11,32 +11,40 @@ namespace Microsoft.AspNetCore.Builder { public static class RazorPagesEndpointRouteBuilderExtensions { - public static IEndpointConventionBuilder MapRazorPages( - this IEndpointRouteBuilder routeBuilder, - string basePath = null) + public static IEndpointConventionBuilder MapRazorPages(this IEndpointRouteBuilder routes) { - var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault(); - - if (mvcEndpointDataSource == null) + if (routes == null) { - mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService(); - routeBuilder.DataSources.Add(mvcEndpointDataSource); + throw new ArgumentNullException(nameof(routes)); } - var conventionBuilder = new DefaultEndpointConventionBuilder(); + EnsureRazorPagesServices(routes); - mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add(actionDescriptor => + return GetOrCreateDataSource(routes); + } + + private static void EnsureRazorPagesServices(IEndpointRouteBuilder routes) + { + var marker = routes.ServiceProvider.GetService(); + if (marker == null) { - if (actionDescriptor is PageActionDescriptor pageActionDescriptor) - { - // TODO: Filter pages by path - return conventionBuilder; - } + throw new InvalidOperationException(Mvc.Core.Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + "AddMvc", + "ConfigureServices(...)")); + } + } - return null; - }); + private static PageActionEndpointDataSource GetOrCreateDataSource(IEndpointRouteBuilder routes) + { + var dataSource = routes.DataSources.OfType().FirstOrDefault(); + if (dataSource == null) + { + dataSource = routes.ServiceProvider.GetRequiredService(); + routes.DataSources.Add(dataSource); + } - return conventionBuilder; + return dataSource; } } } diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index b58349c3db..29f279836e 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -87,6 +87,7 @@ namespace Microsoft.Extensions.DependencyInjection ServiceDescriptor.Singleton()); services.TryAddEnumerable( ServiceDescriptor.Singleton()); + services.TryAddSingleton(); services.TryAddEnumerable( ServiceDescriptor.Singleton()); diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionEndpointDataSource.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionEndpointDataSource.cs new file mode 100644 index 0000000000..d27c1a0a61 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionEndpointDataSource.cs @@ -0,0 +1,43 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + internal class PageActionEndpointDataSource : ActionEndpointDataSourceBase + { + private readonly ActionEndpointFactory _endpointFactory; + + public PageActionEndpointDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) + : base(actions) + { + _endpointFactory = endpointFactory; + + // IMPORTANT: this needs to be the last thing we do in the constructor. + // Change notifications can happen immediately! + Subscribe(); + } + + protected override List CreateEndpoints(IReadOnlyList actions, IReadOnlyList> conventions) + { + var endpoints = new List(); + for (var i = 0; i < actions.Count; i++) + { + if (actions[i] is PageActionDescriptor action) + { + _endpointFactory.AddEndpoints(endpoints, action, Array.Empty(), conventions); + } + } + + return endpoints; + } + } +} + diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs index 11e786561b..7bfbfda4b3 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs @@ -2,9 +2,7 @@ // 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.Diagnostics; -using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder.Internal; using Microsoft.AspNetCore.Mvc.Routing; @@ -64,7 +62,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder } [Fact] - public void UseMvc_EndpointRoutingEnabled_NoEndpointInfos() + public void UseMvc_EndpointRoutingEnabled_AddsRoute() { // Arrange var services = new ServiceCollection(); @@ -85,11 +83,11 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder var routeOptions = appBuilder.ApplicationServices .GetRequiredService>(); - var mvcEndpointDataSource = (MvcEndpointDataSource)Assert.Single(routeOptions.Value.EndpointDataSources, ds => ds is MvcEndpointDataSource); + var dataSource = (ActionEndpointDataSource)Assert.Single(routeOptions.Value.EndpointDataSources, ds => ds is ActionEndpointDataSource); - var endpointInfo = Assert.Single(mvcEndpointDataSource.ConventionalEndpointInfos); - Assert.Equal("default", endpointInfo.Name); - Assert.Equal("{controller=Home}/{action=Index}/{id?}", endpointInfo.Pattern); + var endpointInfo = Assert.Single(dataSource.Routes); + Assert.Equal("default", endpointInfo.RouteName); + Assert.Equal("{controller=Home}/{action=Index}/{id?}", endpointInfo.Pattern.RawText); } } } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceBaseTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceBaseTest.cs new file mode 100644 index 0000000000..e4aacf1cc1 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceBaseTest.cs @@ -0,0 +1,173 @@ +// 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.Threading; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + // There are some basic sanity tests here for the details of how actions + // are turned into endpoints. See ActionEndpointFactoryTest for detailed tests. + public abstract class ActionEndpointDataSourceBaseTest + { + [Fact] + public void Endpoints_AccessParameters_InitializedFromProvider() + { + // Arrange + var actions = new Mock(); + actions.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List + { + CreateActionDescriptor(new { Name = "Value", }, "/Template!"), + }, 0)); + + var dataSource = CreateDataSource(actions.Object); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + var endpoint = Assert.IsType(Assert.Single(endpoints)); + Assert.Equal("Value", endpoint.RoutePattern.RequiredValues["Name"]); + Assert.Equal("/Template!", endpoint.RoutePattern.RawText); + } + + [Fact] + public void Endpoints_CalledMultipleTimes_ReturnsSameInstance() + { + // Arrange + var actionDescriptorCollectionProviderMock = new Mock(); + actionDescriptorCollectionProviderMock + .Setup(m => m.ActionDescriptors) + .Returns(new ActionDescriptorCollection(new[] + { + CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }, "/test"), + }, version: 0)); + + var dataSource = CreateDataSource(actionDescriptorCollectionProviderMock.Object); + + // Act + var endpoints1 = dataSource.Endpoints; + var endpoints2 = dataSource.Endpoints; + + // Assert + Assert.Collection( + endpoints1, + (e) => Assert.Equal("/test", Assert.IsType(e).RoutePattern.RawText)); + Assert.Same(endpoints1, endpoints2); + + actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once); + } + + [Fact] + public void Endpoints_ChangeTokenTriggered_EndpointsRecreated() + { + // Arrange + var actionDescriptorCollectionProviderMock = new Mock(); + actionDescriptorCollectionProviderMock + .Setup(m => m.ActionDescriptors) + .Returns(new ActionDescriptorCollection(new[] + { + CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }, "/test") + }, version: 0)); + + CancellationTokenSource cts = null; + actionDescriptorCollectionProviderMock + .Setup(m => m.GetChangeToken()) + .Returns(() => + { + cts = new CancellationTokenSource(); + var changeToken = new CancellationChangeToken(cts.Token); + + return changeToken; + }); + + var dataSource = CreateDataSource(actionDescriptorCollectionProviderMock.Object); + + // Act + var endpoints = dataSource.Endpoints; + + Assert.Collection(endpoints, + (e) => + { + var routePattern = Assert.IsType(e).RoutePattern; + Assert.Equal("/test", routePattern.RawText); + Assert.Equal("TestController", routePattern.RequiredValues["controller"]); + Assert.Equal("TestAction", routePattern.RequiredValues["action"]); + }); + + actionDescriptorCollectionProviderMock + .Setup(m => m.ActionDescriptors) + .Returns(new ActionDescriptorCollection(new[] + { + CreateActionDescriptor(new { controller = "NewTestController", action = "NewTestAction" }, "/test") + }, version: 1)); + + cts.Cancel(); + + // Assert + var newEndpoints = dataSource.Endpoints; + + Assert.NotSame(endpoints, newEndpoints); + Assert.Collection(newEndpoints, + (e) => + { + var routePattern = Assert.IsType(e).RoutePattern; + Assert.Equal("/test", routePattern.RawText); + Assert.Equal("NewTestController", routePattern.RequiredValues["controller"]); + Assert.Equal("NewTestAction", routePattern.RequiredValues["action"]); + }); + } + + protected private ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions = null) + { + if (actions == null) + { + actions = new DefaultActionDescriptorCollectionProvider( + Array.Empty(), + Array.Empty()); + } + + var services = new ServiceCollection(); + services.AddSingleton(actions); + + var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); + services.Configure(routeOptionsSetup.Configure); + services.AddRouting(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); + + var serviceProvider = services.BuildServiceProvider(); + + var endpointFactory = new ActionEndpointFactory( + serviceProvider.GetRequiredService(), + new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty()))); + + return CreateDataSource(actions, endpointFactory); + } + + protected private abstract ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory); + + private class UpperCaseParameterTransform : IOutboundParameterTransformer + { + public string TransformOutbound(object value) + { + return value?.ToString().ToUpperInvariant(); + } + } + + protected abstract ActionDescriptor CreateActionDescriptor( + object values, + string pattern = null, + IList metadata = null); + } +} \ No newline at end of file diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceTest.cs new file mode 100644 index 0000000000..0cc3ab7959 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointDataSourceTest.cs @@ -0,0 +1,173 @@ +// 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 Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + public class ActionEndpointDataSourceTest : ActionEndpointDataSourceBaseTest + { + [Fact] + public void Endpoints_MultipledActions_MultipleRoutes() + { + // Arrange + var actions = new List + { + new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Test" }, + { "controller", "Test" }, + }, + }, + new ActionDescriptor + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Index" }, + { "controller", "Home" }, + }, + } + }; + + var mockDescriptorProvider = new Mock(); + mockDescriptorProvider + .Setup(m => m.ActionDescriptors) + .Returns(new ActionDescriptorCollection(actions, 0)); + + var dataSource = (ActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object); + dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null)); + dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null)); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + Assert.Collection( + endpoints.Cast().OrderBy(e => e.RoutePattern.RawText), + e => + { + Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/test", e.RoutePattern.RawText); + Assert.Same(actions[0], e.Metadata.GetMetadata()); + }); + } + + [Fact] + public void Endpoints_AppliesConventions() + { + // Arrange + var actions = new List + { + new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Test" }, + { "controller", "Test" }, + }, + }, + new ActionDescriptor + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Index" }, + { "controller", "Home" }, + }, + } + }; + + var mockDescriptorProvider = new Mock(); + mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0)); + + var dataSource = (ActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object); + dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null)); + dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null)); + + dataSource.Add((b) => + { + b.Metadata.Add("Hi there"); + }); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + Assert.Collection( + endpoints.OfType().OrderBy(e => e.RoutePattern.RawText), + e => + { + Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + Assert.Equal("Hi there", e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + Assert.Equal("Hi there", e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/test", e.RoutePattern.RawText); + Assert.Same(actions[0], e.Metadata.GetMetadata()); + Assert.Equal("Hi there", e.Metadata.GetMetadata()); + }); + } + + private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) + { + return new ActionEndpointDataSource(actions, endpointFactory); + } + + protected override ActionDescriptor CreateActionDescriptor( + object values, + string pattern = null, + IList metadata = null) + { + var action = new ActionDescriptor(); + + foreach (var kvp in new RouteValueDictionary(values)) + { + action.RouteValues[kvp.Key] = kvp.Value?.ToString(); + } + + if (!string.IsNullOrEmpty(pattern)) + { + action.AttributeRouteInfo = new AttributeRouteInfo + { + Name = "test", + Template = pattern, + }; + } + + action.EndpointMetadata = metadata; + return action; + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointFactoryTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointFactoryTest.cs new file mode 100644 index 0000000000..c4bb99e799 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionEndpointFactoryTest.cs @@ -0,0 +1,498 @@ +// 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.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + public class ActionEndpointFactoryTest + { + public ActionEndpointFactoryTest() + { + var serviceCollection = new ServiceCollection(); + + var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); + serviceCollection.Configure(routeOptionsSetup.Configure); + serviceCollection.AddRouting(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); + + Services = serviceCollection.BuildServiceProvider(); + InvokerFactory = new Mock(MockBehavior.Strict); + Factory = new ActionEndpointFactory( + Services.GetRequiredService(), + new MvcEndpointInvokerFactory(InvokerFactory.Object)); + } + + internal ActionEndpointFactory Factory { get; } + + internal Mock InvokerFactory { get; } + + internal IServiceProvider Services { get; } + + [Fact] + public async Task AddEndpoints_AttributeRouted_UsesActionInvoker() + { + // Arrange + var values = new + { + action = "Test", + controller = "Test", + page = (string)null, + }; + + var action = CreateActionDescriptor(values, pattern: "/Test"); + + var endpointFeature = new EndpointSelectorContext + { + RouteValues = new RouteValueDictionary() + }; + + var featureCollection = new FeatureCollection(); + featureCollection.Set(endpointFeature); + featureCollection.Set(endpointFeature); + featureCollection.Set(endpointFeature); + + var httpContextMock = new Mock(); + httpContextMock.Setup(m => m.Features).Returns(featureCollection); + + var actionInvokerCalled = false; + var actionInvokerMock = new Mock(); + actionInvokerMock.Setup(m => m.InvokeAsync()).Returns(() => + { + actionInvokerCalled = true; + return Task.CompletedTask; + }); + + InvokerFactory + .Setup(m => m.CreateInvoker(It.IsAny())) + .Returns(actionInvokerMock.Object); + + // Act + var endpoint = CreateAttributeRoutedEndpoint(action); + + // Assert + await endpoint.RequestDelegate(httpContextMock.Object); + + Assert.True(actionInvokerCalled); + } + + [Fact] + public async Task AddEndpoints_ConventionalRouted_UsesActionInvoker() + { + // Arrange + var values = new + { + action = "Test", + controller = "Test", + page = (string)null, + }; + + var action = CreateActionDescriptor(values); + + var endpointFeature = new EndpointSelectorContext + { + RouteValues = new RouteValueDictionary() + }; + + var featureCollection = new FeatureCollection(); + featureCollection.Set(endpointFeature); + featureCollection.Set(endpointFeature); + featureCollection.Set(endpointFeature); + + var httpContextMock = new Mock(); + httpContextMock.Setup(m => m.Features).Returns(featureCollection); + + var actionInvokerCalled = false; + var actionInvokerMock = new Mock(); + actionInvokerMock.Setup(m => m.InvokeAsync()).Returns(() => + { + actionInvokerCalled = true; + return Task.CompletedTask; + }); + + InvokerFactory + .Setup(m => m.CreateInvoker(It.IsAny())) + .Returns(actionInvokerMock.Object); + + // Act + var endpoint = CreateConventionalRoutedEndpoint(action, "{controller}/{action}"); + + // Assert + await endpoint.RequestDelegate(httpContextMock.Object); + + Assert.True(actionInvokerCalled); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_WithEmptyRouteName_CreatesMetadataWithEmptyRouteName() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute(routeName: string.Empty, pattern: "{controller}/{action}"); + + // Act + var endpoint = CreateConventionalRoutedEndpoint(action, route); + + // Assert + var routeNameMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(routeNameMetadata); + Assert.Equal(string.Empty, routeNameMetadata.RouteName); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_ContainsParameterWithNullRequiredRouteValue_NoEndpointCreated() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute( + routeName: "Test", + pattern: "{controller}/{action}/{page}", + defaults: new RouteValueDictionary(new { action = "TestAction" })); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Empty(endpoints); + } + + // area, controller, action and page are special, but not hardcoded. Actions can define custom required + // route values. This has been used successfully for localization, versioning and similar schemes. We should + // be able to replace custom route values too. + [Fact] + public void AddEndpoints_ConventionalRouted_NonReservedRequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint() + { + // Arrange + var values = new { controller = "home", action = "index", locale = "en-NZ" }; + var action = CreateActionDescriptor(values); + var route = CreateRoute(routeName: "test", pattern: "{controller}/{action}"); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_NonReservedRequiredValue_WithCorresponding_TemplateParameter_ProducesEndpoint() + { + // Arrange + var values = new { controller = "home", action = "index", locale = "en-NZ" }; + var action = CreateActionDescriptor(values); + var route = CreateRoute(routeName: "test", pattern: "{locale}/{controller}/{action}"); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Single(endpoints); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_NonAreaRouteForAreaAction_DoesNotProduceEndpoint() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", area = "admin", page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute(routeName: "test", pattern: "{controller}/{action}"); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_AreaRouteForNonAreaAction_DoesNotProduceEndpoint() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", area = (string)null, page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute(routeName: "test", pattern: "{area}/{controller}/{action}"); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_RequiredValues_DoesNotMatchParameterDefaults_CreatesEndpoint() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute( + routeName: "test", + pattern: "{controller}/{action}/{id?}", + defaults: new RouteValueDictionary(new { controller = "TestController", action = "TestAction1" })); + + // Act + var endpoint = CreateConventionalRoutedEndpoint(action, route); + + // Assert + Assert.Equal("{controller}/{action}/{id?}", endpoint.RoutePattern.RawText); + Assert.Equal("TestController", endpoint.RoutePattern.RequiredValues["controller"]); + Assert.Equal("TestAction", endpoint.RoutePattern.RequiredValues["action"]); + Assert.Equal("TestController", endpoint.RoutePattern.Defaults["controller"]); + Assert.False(endpoint.RoutePattern.Defaults.ContainsKey("action")); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_RequiredValues_DoesNotMatchNonParameterDefaults_DoesNotProduceEndpoint() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute( + routeName: "test", + pattern: "/Blog/{*slug}", + defaults: new RouteValueDictionary(new { controller = "TestController", action = "TestAction1" })); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_AttributeRoutes_DefaultDifferentCaseFromRouteValue_UseDefaultCase() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", page = (string)null }; + var action = CreateActionDescriptor(values, "{controller}/{action=TESTACTION}/{id?}"); + // Act + var endpoint = CreateAttributeRoutedEndpoint(action); + + // Assert + Assert.Equal("{controller}/{action=TESTACTION}/{id?}", endpoint.RoutePattern.RawText); + Assert.Equal("TESTACTION", endpoint.RoutePattern.Defaults["action"]); + Assert.Equal(0, endpoint.Order); + Assert.Equal("TestAction", endpoint.RoutePattern.RequiredValues["action"]); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_RequiredValueWithNoCorrespondingParameter_DoesNotProduceEndpoint() + { + // Arrange + var values = new { area = "admin", controller = "home", action = "index" }; + var action = CreateActionDescriptor(values); + var route = CreateRoute(routeName: "test", pattern: "{controller}/{action}"); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void AddEndpoints_AttributeRouted_ContainsParameterWithNullRequiredRouteValue_EndpointCreated() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", page = (string)null }; + var action = CreateActionDescriptor(values, "{controller}/{action}/{page}"); + + // Act + var endpoint = CreateAttributeRoutedEndpoint(action); + + // Assert + Assert.Equal("{controller}/{action}/{page}", endpoint.RoutePattern.RawText); + Assert.Equal("TestController", endpoint.RoutePattern.RequiredValues["controller"]); + Assert.Equal("TestAction", endpoint.RoutePattern.RequiredValues["action"]); + Assert.False(endpoint.RoutePattern.RequiredValues.ContainsKey("page")); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_WithMatchingConstraint_CreatesEndpoint() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction1", page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute( + routeName: "test", + pattern: "{controller}/{action}", + constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" })); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Single(endpoints); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_WithNotMatchingConstraint_DoesNotCreateEndpoint() + { + // Arrange + var values = new { controller = "TestController", action = "TestAction", page = (string)null }; + var action = CreateActionDescriptor(values); + var route = CreateRoute( + routeName: "test", + pattern: "{controller}/{action}", + constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" })); + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, route); + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void AddEndpoints_ConventionalRouted_StaticallyDefinedOrder_IsMaintained() + { + // Arrange + var values = new { controller = "Home", action = "Index", page = (string)null }; + var action = CreateActionDescriptor(values); + var routes = new[] + { + CreateRoute(routeName: "test1", pattern: "{controller}/{action}/{id?}"), + CreateRoute(routeName: "test2", pattern: "named/{controller}/{action}/{id?}"), + }; + + // Act + var endpoints = CreateConventionalRoutedEndpoints(action, routes); + + // Assert + Assert.Collection( + endpoints, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + Assert.Equal("{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); + Assert.Equal("Index", matcherEndpoint.RoutePattern.RequiredValues["action"]); + Assert.Equal("Home", matcherEndpoint.RoutePattern.RequiredValues["controller"]); + Assert.Equal(1, matcherEndpoint.Order); + }, + (ep) => + { + var matcherEndpoint = Assert.IsType(ep); + Assert.Equal("named/{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); + Assert.Equal("Index", matcherEndpoint.RoutePattern.RequiredValues["action"]); + Assert.Equal("Home", matcherEndpoint.RoutePattern.RequiredValues["controller"]); + Assert.Equal(2, matcherEndpoint.Order); + }); + } + + private RouteEndpoint CreateAttributeRoutedEndpoint(ActionDescriptor action) + { + var endpoints = new List(); + Factory.AddEndpoints(endpoints, action, Array.Empty(), Array.Empty>()); + return Assert.IsType(Assert.Single(endpoints)); + } + + private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, string template) + { + return CreateConventionalRoutedEndpoint(action, new ConventionalRouteEntry(routeName: null, template, null, null, null)); + } + + private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, ConventionalRouteEntry route) + { + Assert.NotNull(action.RouteValues); + + var endpoints = new List(); + Factory.AddEndpoints(endpoints, action, new[] { route, }, Array.Empty>()); + var endpoint = Assert.IsType(Assert.Single(endpoints)); + + // This should be true for all conventional-routed actions. + AssertIsSubset(new RouteValueDictionary(action.RouteValues), endpoint.RoutePattern.RequiredValues); + + return endpoint; + } + + private IReadOnlyList CreateConventionalRoutedEndpoints(ActionDescriptor action, ConventionalRouteEntry route) + { + return CreateConventionalRoutedEndpoints(action, new[] { route, }); + } + + private IReadOnlyList CreateConventionalRoutedEndpoints(ActionDescriptor action, IReadOnlyList routes) + { + var endpoints = new List(); + Factory.AddEndpoints(endpoints, action, routes, Array.Empty>()); + return endpoints.Cast().ToList(); + } + + private ConventionalRouteEntry CreateRoute( + string routeName, + string pattern, + RouteValueDictionary defaults = null, + IDictionary constraints = null, + RouteValueDictionary dataTokens = null) + { + return new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens); + } + + private ActionDescriptor CreateActionDescriptor( + object requiredValues, + string pattern = null, + IList metadata = null) + { + var actionDescriptor = new ActionDescriptor(); + var routeValues = new RouteValueDictionary(requiredValues); + foreach (var kvp in routeValues) + { + actionDescriptor.RouteValues[kvp.Key] = kvp.Value?.ToString(); + } + + if (!string.IsNullOrEmpty(pattern)) + { + actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo + { + Name = pattern, + Template = pattern + }; + } + + actionDescriptor.EndpointMetadata = metadata; + return actionDescriptor; + } + + private void AssertIsSubset( + IReadOnlyDictionary subset, + IReadOnlyDictionary fullSet) + { + foreach (var subsetPair in subset) + { + var isPresent = fullSet.TryGetValue(subsetPair.Key, out var fullSetPairValue); + Assert.True(isPresent); + Assert.Equal(subsetPair.Value, fullSetPairValue); + } + } + + private void AssertMatchingSuppressed(Endpoint endpoint, bool suppressed) + { + var isEndpointSuppressed = endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false; + Assert.Equal(suppressed, isEndpointSuppressed); + } + + private class UpperCaseParameterTransform : IOutboundParameterTransformer + { + public string TransformOutbound(object value) + { + return value?.ToString().ToUpperInvariant(); + } + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerActionEndpointDataSourceTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerActionEndpointDataSourceTest.cs new file mode 100644 index 0000000000..d58c1933da --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerActionEndpointDataSourceTest.cs @@ -0,0 +1,206 @@ +// 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 Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + public class ControllerActionEndpointDataSourceTest : ActionEndpointDataSourceBaseTest + { + [Fact] + public void Endpoints_Ignores_NonController() + { + // Arrange + var actions = new List + { + new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Test" }, + { "controller", "Test" }, + }, + }, + }; + + var mockDescriptorProvider = new Mock(); + mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0)); + + var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + Assert.Empty(endpoints); + } + + [Fact] + public void Endpoints_MultipledActions_MultipleRoutes() + { + // Arrange + var actions = new List + { + new ControllerActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Test" }, + { "controller", "Test" }, + }, + }, + new ControllerActionDescriptor + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Index" }, + { "controller", "Home" }, + }, + } + }; + + var mockDescriptorProvider = new Mock(); + mockDescriptorProvider + .Setup(m => m.ActionDescriptors) + .Returns(new ActionDescriptorCollection(actions, 0)); + + var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object); + dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null)); + dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null)); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + Assert.Collection( + endpoints.Cast().OrderBy(e => e.RoutePattern.RawText), + e => + { + Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/test", e.RoutePattern.RawText); + Assert.Same(actions[0], e.Metadata.GetMetadata()); + }); + } + + [Fact] + public void Endpoints_AppliesConventions() + { + // Arrange + var actions = new List + { + new ControllerActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Test" }, + { "controller", "Test" }, + }, + }, + new ControllerActionDescriptor + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Index" }, + { "controller", "Home" }, + }, + } + }; + + var mockDescriptorProvider = new Mock(); + mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0)); + + var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object); + dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null)); + dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null)); + + dataSource.Add((b) => + { + b.Metadata.Add("Hi there"); + }); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + Assert.Collection( + endpoints.OfType().OrderBy(e => e.RoutePattern.RawText), + e => + { + Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + Assert.Equal("Hi there", e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText); + Assert.Same(actions[1], e.Metadata.GetMetadata()); + Assert.Equal("Hi there", e.Metadata.GetMetadata()); + }, + e => + { + Assert.Equal("/test", e.RoutePattern.RawText); + Assert.Same(actions[0], e.Metadata.GetMetadata()); + Assert.Equal("Hi there", e.Metadata.GetMetadata()); + }); + } + + private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) + { + return new ControllerActionEndpointDataSource(actions, endpointFactory); + } + + protected override ActionDescriptor CreateActionDescriptor( + object values, + string pattern = null, + IList metadata = null) + { + var action = new ControllerActionDescriptor(); + + foreach (var kvp in new RouteValueDictionary(values)) + { + action.RouteValues[kvp.Key] = kvp.Value?.ToString(); + } + + if (!string.IsNullOrEmpty(pattern)) + { + action.AttributeRouteInfo = new AttributeRouteInfo + { + Name = "test", + Template = pattern, + }; + } + + action.EndpointMetadata = metadata; + return action; + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/MvcEndpointDataSourceTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/MvcEndpointDataSourceTests.cs deleted file mode 100644 index d1c923b685..0000000000 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/MvcEndpointDataSourceTests.cs +++ /dev/null @@ -1,806 +0,0 @@ -// 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; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Primitives; -using Moq; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Routing -{ - public class MvcEndpointDataSourceTests - { - [Fact] - public void Endpoints_AccessParameters_InitializedFromProvider() - { - // Arrange - var routeValue = "Value"; - var requiredValues = new Dictionary - { - ["Name"] = routeValue - }; - var displayName = "DisplayName!"; - var order = 1; - var template = "/Template!"; - var filterDescriptor = new FilterDescriptor(new ControllerActionFilter(), 1); - - var mockDescriptorProvider = new Mock(); - mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List - { - new ActionDescriptor - { - RouteValues = requiredValues, - DisplayName = displayName, - AttributeRouteInfo = new AttributeRouteInfo - { - Order = order, - Template = template - }, - FilterDescriptors = new List - { - filterDescriptor - } - } - }, 0)); - - var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - var endpoint = Assert.Single(endpoints); - var matcherEndpoint = Assert.IsType(endpoint); - - var endpointValue = matcherEndpoint.RoutePattern.RequiredValues["Name"]; - Assert.Equal(routeValue, endpointValue); - - Assert.Equal(displayName, matcherEndpoint.DisplayName); - Assert.Equal(order, matcherEndpoint.Order); - Assert.Equal("/Template!", matcherEndpoint.RoutePattern.RawText); - } - - [Fact] - public async Task Endpoints_InvokeReturnedEndpoint_ActionInvokerProviderCalled() - { - // Arrange - var endpointFeature = new EndpointSelectorContext - { - RouteValues = new RouteValueDictionary() - }; - - var featureCollection = new FeatureCollection(); - featureCollection.Set(endpointFeature); - featureCollection.Set(endpointFeature); - featureCollection.Set(endpointFeature); - - var httpContextMock = new Mock(); - httpContextMock.Setup(m => m.Features).Returns(featureCollection); - - var descriptorProviderMock = new Mock(); - descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List - { - new ActionDescriptor - { - AttributeRouteInfo = new AttributeRouteInfo - { - Template = string.Empty - }, - FilterDescriptors = new List() - } - }, 0)); - - var actionInvokerCalled = false; - var actionInvokerMock = new Mock(); - actionInvokerMock.Setup(m => m.InvokeAsync()).Returns(() => - { - actionInvokerCalled = true; - return Task.CompletedTask; - }); - - var actionInvokerProviderMock = new Mock(); - actionInvokerProviderMock.Setup(m => m.CreateInvoker(It.IsAny())).Returns(actionInvokerMock.Object); - - var dataSource = CreateMvcEndpointDataSource( - descriptorProviderMock.Object, - new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object)); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - var endpoint = Assert.Single(endpoints); - var matcherEndpoint = Assert.IsType(endpoint); - - await matcherEndpoint.RequestDelegate(httpContextMock.Object); - - Assert.True(actionInvokerCalled); - } - - [Fact] - public void Endpoints_SingleAction_ConventionalRoute_ContainsParameterWithNullRequiredRouteValue() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction", page = (string)null }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - string.Empty, - "{controller}/{action}/{page}", - new RouteValueDictionary(new { action = "TestAction" }))); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Empty(endpoints); - } - - [Fact] - public void Endpoints_SingleAction_AttributeRoute_ContainsParameterWithNullRequiredRouteValue() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - "{controller}/{action}/{page}", - new { controller = "TestController", action = "TestAction", page = (string)null }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection(endpoints.Cast(), - (e) => - { - Assert.Equal("{controller}/{action}/{page}", e.RoutePattern.RawText); - Assert.Equal("TestController", e.RoutePattern.RequiredValues["controller"]); - Assert.Equal("TestAction", e.RoutePattern.RequiredValues["action"]); - Assert.False(e.RoutePattern.RequiredValues.ContainsKey("page")); - }); - } - - [Fact] - public void Endpoints_CalledMultipleTimes_ReturnsSameInstance() - { - // Arrange - var actionDescriptorCollectionProviderMock = new Mock(); - actionDescriptorCollectionProviderMock - .Setup(m => m.ActionDescriptors) - .Returns(new ActionDescriptorCollection(new[] - { - CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }) - }, version: 0)); - - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollectionProviderMock.Object); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - string.Empty, - "{controller}/{action}", - new RouteValueDictionary(new { action = "TestAction" }))); - - // Act - var endpoints1 = dataSource.Endpoints; - var endpoints2 = dataSource.Endpoints; - - // Assert - Assert.Collection(endpoints1, - (e) => Assert.Equal("{controller}/{action}", Assert.IsType(e).RoutePattern.RawText)); - Assert.Same(endpoints1, endpoints2); - - actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once); - } - - [Fact] - public void Endpoints_ChangeTokenTriggered_EndpointsRecreated() - { - // Arrange - var actionDescriptorCollectionProviderMock = new Mock(); - actionDescriptorCollectionProviderMock - .Setup(m => m.ActionDescriptors) - .Returns(new ActionDescriptorCollection(new[] - { - CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }) - }, version: 0)); - - CancellationTokenSource cts = null; - actionDescriptorCollectionProviderMock - .Setup(m => m.GetChangeToken()) - .Returns(() => - { - cts = new CancellationTokenSource(); - var changeToken = new CancellationChangeToken(cts.Token); - - return changeToken; - }); - - - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollectionProviderMock.Object); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - string.Empty, - "{controller}/{action}", - new RouteValueDictionary(new { action = "TestAction" }))); - - // Act - var endpoints = dataSource.Endpoints; - - Assert.Collection(endpoints, - (e) => - { - var routePattern = Assert.IsType(e).RoutePattern; - Assert.Equal("{controller}/{action}", routePattern.RawText); - Assert.Equal("TestController", routePattern.RequiredValues["controller"]); - Assert.Equal("TestAction", routePattern.RequiredValues["action"]); - }); - - actionDescriptorCollectionProviderMock - .Setup(m => m.ActionDescriptors) - .Returns(new ActionDescriptorCollection(new[] - { - CreateActionDescriptor(new { controller = "NewTestController", action = "NewTestAction" }) - }, version: 1)); - - cts.Cancel(); - - // Assert - var newEndpoints = dataSource.Endpoints; - - Assert.NotSame(endpoints, newEndpoints); - Assert.Collection(newEndpoints, - (e) => - { - var routePattern = Assert.IsType(e).RoutePattern; - Assert.Equal("{controller}/{action}", routePattern.RawText); - Assert.Equal("NewTestController", routePattern.RequiredValues["controller"]); - Assert.Equal("NewTestAction", routePattern.RequiredValues["action"]); - }); - } - - [Fact] - public void Endpoints_MultipleActions_WithActionConstraint() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }, - new { controller = "TestController", action = "TestAction1" }, - new { controller = "TestController", action = "TestAction2" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - string.Empty, - "{controller}/{action}", - constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" }))); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection(endpoints.Cast(), - (e) => - { - Assert.Equal("{controller}/{action}", e.RoutePattern.RawText); - Assert.Equal("TestController", e.RoutePattern.RequiredValues["controller"]); - Assert.Equal("TestAction1", e.RoutePattern.RequiredValues["action"]); - }, - (e) => - { - Assert.Equal("{controller}/{action}", e.RoutePattern.RawText); - Assert.Equal("TestController", e.RoutePattern.RequiredValues["controller"]); - Assert.Equal("TestAction2", e.RoutePattern.RequiredValues["action"]); - }); - } - - [Fact] - public void Endpoints_ConventionalRoute_WithEmptyRouteName_CreatesMetadataWithEmptyRouteName() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "Home", action = "Index" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add( - CreateEndpointInfo(string.Empty, "named/{controller}/{action}/{id?}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - var endpoint = Assert.Single(endpoints); - var matcherEndpoint = Assert.IsType(endpoint); - var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); - Assert.NotNull(routeNameMetadata); - Assert.Equal(string.Empty, routeNameMetadata.RouteName); - } - - [Fact] - public void Endpoints_CanCreateMultipleEndpoints_WithSameRouteName() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "Home", action = "Index" }, - new { controller = "Products", action = "Details" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add( - CreateEndpointInfo("namedRoute", "named/{controller}/{action}/{id?}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); - Assert.NotNull(routeNameMetadata); - Assert.Equal("namedRoute", routeNameMetadata.RouteName); - Assert.Equal("named/{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); - Assert.NotNull(routeNameMetadata); - Assert.Equal("namedRoute", routeNameMetadata.RouteName); - Assert.Equal("named/{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); - }); - } - - [Fact] - public void Endpoints_ConventionalRoutes_StaticallyDefinedOrder_IsMaintained() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "Home", action = "Index" }, - new { controller = "Products", action = "Details" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller}/{action}/{id?}")); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: "namedRoute", - "named/{controller}/{action}/{id?}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("Index", matcherEndpoint.RoutePattern.RequiredValues["action"]); - Assert.Equal("Home", matcherEndpoint.RoutePattern.RequiredValues["controller"]); - Assert.Equal(1, matcherEndpoint.Order); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("named/{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("Index", matcherEndpoint.RoutePattern.RequiredValues["action"]); - Assert.Equal("Home", matcherEndpoint.RoutePattern.RequiredValues["controller"]); - Assert.Equal(2, matcherEndpoint.Order); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("Details", matcherEndpoint.RoutePattern.RequiredValues["action"]); - Assert.Equal("Products", matcherEndpoint.RoutePattern.RequiredValues["controller"]); - Assert.Equal(1, matcherEndpoint.Order); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("named/{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("Details", matcherEndpoint.RoutePattern.RequiredValues["action"]); - Assert.Equal("Products", matcherEndpoint.RoutePattern.RequiredValues["controller"]); - Assert.Equal(2, matcherEndpoint.Order); - }); - } - - [Fact] - public void RequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint() - { - // Arrange - var requiredValues = new RouteValueDictionary(new { area = "admin", controller = "home", action = "index" }); - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Empty(endpoints); - } - - // area, controller, action and page are special, but not hardcoded. Actions can define custom required - // route values. This has been used successfully for localization, versioning and similar schemes. We should - // be able to replace custom route values too. - [Fact] - public void NonReservedRequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint() - { - // Arrange - var action1 = new RouteValueDictionary(new { controller = "home", action = "index", locale = "en-NZ" }); - var action2 = new RouteValueDictionary(new { controller = "home", action = "about", locale = "en-CA" }); - var action3 = new RouteValueDictionary(new { controller = "home", action = "index", locale = (string)null }); - - var actionDescriptorCollection = GetActionDescriptorCollection(action1, action2, action3); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - - // Adding a localized route a non-localized route - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{locale}/{controller}/{action}")); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints.Cast(), - e => - { - Assert.Equal("{locale}/{controller}/{action}", e.RoutePattern.RawText); - Assert.Equal("home", e.RoutePattern.RequiredValues["controller"]); - Assert.Equal("index", e.RoutePattern.RequiredValues["action"]); - Assert.Equal("en-NZ", e.RoutePattern.RequiredValues["locale"]); - }, - e => - { - Assert.Equal("{locale}/{controller}/{action}", e.RoutePattern.RawText); - Assert.Equal("home", e.RoutePattern.RequiredValues["controller"]); - Assert.Equal("about", e.RoutePattern.RequiredValues["action"]); - Assert.Equal("en-CA", e.RoutePattern.RequiredValues["locale"]); - }, - e => - { - Assert.Equal("{controller}/{action}", e.RoutePattern.RawText); - Assert.Equal("home", e.RoutePattern.RequiredValues["controller"]); - Assert.Equal("index", e.RoutePattern.RequiredValues["action"]); - }); - } - - [Fact] - public void TemplateParameter_WithNoDefaultOrRequiredValue_DoesNotProduceEndpoint() - { - // Arrange - var requiredValues = new RouteValueDictionary(new { controller = "home", action = "index", area = (string)null }); - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Empty(endpoints); - } - - [Fact] - public void TemplateParameter_WithDefaultValue_AndNullRequiredValue_DoesNotProduceEndpoint() - { - // Arrange - var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" }); - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area=admin}/{controller}/{action}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Empty(endpoints); - } - - [Fact] - public void TemplateParameter_WithNullRequiredValue_DoesNotProduceEndpoint() - { - // Arrange - var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" }); - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Empty(endpoints); - } - - [Fact] - public void NoDefaultValues_RequiredValues_UsedToCreateDefaultValues() - { - // Arrange - var requiredValues = new RouteValueDictionary(new { controller = "Foo", action = "Bar" }); - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - var endpoint = Assert.Single(endpoints); - var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("{controller}/{action}", matcherEndpoint.RoutePattern.RawText); - AssertIsSubset(requiredValues, matcherEndpoint.RoutePattern.RequiredValues); - } - - [Fact] - public void RequiredValues_NotPresent_InDefaultValuesOrParameter_EndpointNotCreated() - { - // Arrange - var requiredValues = new RouteValueDictionary( - new { controller = "Foo", action = "Bar", subarea = "test" }); - var expectedDefaults = requiredValues; - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add( - CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Empty(endpoints); - } - - [Fact] - public void RequiredValues_DoesNotMatchParameterDefaults_Included() - { - // Arrange - var action = new RouteValueDictionary( - new { controller = "Foo", action = "Baz", }); // Doesn't match default - var expectedDefaults = new RouteValueDictionary( - new { controller = "Foo", action = "Baz", }); - var actionDescriptorCollection = GetActionDescriptorCollection(action); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add( - CreateEndpointInfo( - string.Empty, - "{controller}/{action}/{id?}", - defaults: new RouteValueDictionary(new { controller = "Foo", action = "Bar" }))); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - var endpoint = Assert.Single(endpoints); - var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("Foo", matcherEndpoint.RoutePattern.RequiredValues["controller"]); - Assert.Equal("Baz", matcherEndpoint.RoutePattern.RequiredValues["action"]); - Assert.Equal("Foo", matcherEndpoint.RoutePattern.Defaults["controller"]); - Assert.False(matcherEndpoint.RoutePattern.Defaults.ContainsKey("action")); - } - - [Fact] - public void RequiredValues_DoesNotMatchNonParameterDefaults_FilteredOut() - { - // Arrange - var action1 = new RouteValueDictionary( - new { controller = "Foo", action = "Bar", }); - var action2 = new RouteValueDictionary( - new { controller = "Foo", action = "Baz", }); // Doesn't match default - var expectedDefaults = new RouteValueDictionary( - new { controller = "Foo", action = "Bar", }); - var actionDescriptorCollection = GetActionDescriptorCollection(action1, action2); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add( - CreateEndpointInfo( - string.Empty, - "Blog/{*slug}", - defaults: new RouteValueDictionary(new { controller = "Foo", action = "Bar" }))); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - var endpoint = Assert.Single(endpoints); - var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("Blog/{*slug}", matcherEndpoint.RoutePattern.RawText); - AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); - } - - [Fact] - public void Endpoints_ConventionalRoutes_DefaultValuesAndCatchAll_EndpointInfoDefaultsNotModified() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - - var endpointInfo = CreateEndpointInfo( - name: string.Empty, - defaults: new RouteValueDictionary(), - template: "{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}"); - dataSource.ConventionalEndpointInfos.Add(endpointInfo); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Empty(endpointInfo.Defaults); - } - - [Fact] - public void Endpoints_AttributeRoutes_DefaultDifferentCaseFromRouteValue_UseDefaultCase() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - "{controller}/{action=TESTACTION}/{id?}", - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller}/{action=TESTACTION}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("TESTACTION", matcherEndpoint.RoutePattern.Defaults["action"]); - Assert.Equal(0, matcherEndpoint.Order); - - Assert.Equal("TestAction", matcherEndpoint.RoutePattern.RequiredValues["action"]); - }); - } - - private MvcEndpointDataSource CreateMvcEndpointDataSource( - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null, - MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null) - { - if (actionDescriptorCollectionProvider == null) - { - actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider( - Array.Empty(), - Array.Empty()); - } - - var services = new ServiceCollection(); - services.AddSingleton(actionDescriptorCollectionProvider); - - var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); - services.Configure(routeOptionsSetup.Configure); - services.AddRouting(options => - { - options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); - }); - - var serviceProvider = services.BuildServiceProvider(); - - var dataSource = new MvcEndpointDataSource( - actionDescriptorCollectionProvider, - mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService()); - - var defaultEndpointConventionBuilder = new DefaultEndpointConventionBuilder(); - dataSource.AttributeRoutingConventionResolvers.Add((actionDescriptor) => - { - return defaultEndpointConventionBuilder; - }); - - return dataSource; - } - - private class UpperCaseParameterTransform : IOutboundParameterTransformer - { - public string TransformOutbound(object value) - { - return value?.ToString().ToUpperInvariant(); - } - } - - private MvcEndpointInfo CreateEndpointInfo( - string name, - string template, - RouteValueDictionary defaults = null, - IDictionary constraints = null, - RouteValueDictionary dataTokens = null, - IServiceProvider serviceProvider = null) - { - if (serviceProvider == null) - { - var services = new ServiceCollection(); - services.AddRouting(); - services.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform()); - - var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); - services.Configure(routeOptionsSetup.Configure); - services.Configure(options => - { - options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); - }); - - serviceProvider = services.BuildServiceProvider(); - } - - var parameterPolicyFactory = serviceProvider.GetRequiredService(); - return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, parameterPolicyFactory); - } - - private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues) - { - return GetActionDescriptorCollection(attributeRouteTemplate: null, requiredValues); - } - - private IActionDescriptorCollectionProvider GetActionDescriptorCollection(string attributeRouteTemplate, params object[] requiredValues) - { - var actionDescriptors = new List(); - foreach (var requiredValue in requiredValues) - { - actionDescriptors.Add(CreateActionDescriptor(requiredValue, attributeRouteTemplate)); - } - - return GetActionDescriptorCollection(actionDescriptors.ToArray()); - } - - private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params ActionDescriptor[] actionDescriptors) - { - var actionDescriptorCollectionProviderMock = new Mock(); - actionDescriptorCollectionProviderMock - .Setup(m => m.ActionDescriptors) - .Returns(new ActionDescriptorCollection(actionDescriptors, version: 0)); - return actionDescriptorCollectionProviderMock.Object; - } - - private ActionDescriptor CreateActionDescriptor( - object requiredValues, - string attributeRouteTemplate = null, - IList metadata = null) - { - var actionDescriptor = new ActionDescriptor(); - var routeValues = new RouteValueDictionary(requiredValues); - foreach (var kvp in routeValues) - { - actionDescriptor.RouteValues[kvp.Key] = kvp.Value?.ToString(); - } - if (!string.IsNullOrEmpty(attributeRouteTemplate)) - { - actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo - { - Name = attributeRouteTemplate, - Template = attributeRouteTemplate - }; - } - actionDescriptor.EndpointMetadata = metadata; - return actionDescriptor; - } - - private void AssertIsSubset( - IReadOnlyDictionary subset, - IReadOnlyDictionary fullSet) - { - foreach (var subsetPair in subset) - { - var isPresent = fullSet.TryGetValue(subsetPair.Key, out var fullSetPairValue); - Assert.True(isPresent); - Assert.Equal(subsetPair.Value, fullSetPairValue); - } - } - - private void AssertMatchingSuppressed(Endpoint endpoint, bool suppressed) - { - var isEndpointSuppressed = endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false; - Assert.Equal(suppressed, isEndpointSuppressed); - } - } -} \ No newline at end of file diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs index 746f949205..95ee4cd93a 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs @@ -10,9 +10,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class AntiforgeryTests : IClassFixture> + public class AntiforgeryTests : IClassFixture> { - public AntiforgeryTests(MvcTestFixture fixture) + public AntiforgeryTests(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs index 370db1ebcb..4b6b55736e 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiBehaviorTest.cs @@ -17,9 +17,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class ApiBehaviorTest : IClassFixture> + public class ApiBehaviorTest : IClassFixture> { - public ApiBehaviorTest(MvcTestFixture fixture) + public ApiBehaviorTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AsyncActionsTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AsyncActionsTests.cs index 9f932f87a2..dc8e634ef4 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AsyncActionsTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AsyncActionsTests.cs @@ -5,9 +5,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class AsyncActionsTests : IClassFixture> + public class AsyncActionsTests : IClassFixture> { - public AsyncActionsTests(MvcTestFixture fixture) + public AsyncActionsTests(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs index 102dbcd811..ccb8189ac2 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs @@ -15,7 +15,7 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class BasicTests : IClassFixture> + public class BasicTests : IClassFixture> { // Some tests require comparing the actual response body against an expected response baseline // so they require a reference to the assembly on which the resources are located, in order to @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // use it on all the rest of the tests. private static readonly Assembly _resourcesAssembly = typeof(BasicTests).GetTypeInfo().Assembly; - public BasicTests(MvcTestFixture fixture) + public BasicTests(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs index d6f95edc2e..6e3ccce528 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs @@ -11,9 +11,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class ComponentRenderingFunctionalTests : IClassFixture> + public class ComponentRenderingFunctionalTests : IClassFixture> { - public ComponentRenderingFunctionalTests(MvcTestFixture fixture) + public ComponentRenderingFunctionalTests(MvcTestFixture fixture) { Client = Client ?? CreateClient(fixture); } @@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { } - private HttpClient CreateClient(MvcTestFixture fixture) + private HttpClient CreateClient(MvcTestFixture fixture) { var loopHandler = new LoopHttpHandler(); diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeEndpointRoutingTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeEndpointRoutingTests.cs index 7377442ac6..2165e03f83 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeEndpointRoutingTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeEndpointRoutingTests.cs @@ -8,9 +8,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class ConsumesAttributeEndpointRoutingTests : ConsumesAttributeTestsBase + public class ConsumesAttributeEndpointRoutingTests : ConsumesAttributeTestsBase { - public ConsumesAttributeEndpointRoutingTests(MvcTestFixture fixture) + public ConsumesAttributeEndpointRoutingTests(MvcTestFixture fixture) : base(fixture) { } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs index d694aad1c4..7f88317035 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs @@ -8,9 +8,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class ConsumesAttributeTests : ConsumesAttributeTestsBase + public class ConsumesAttributeTests : ConsumesAttributeTestsBase { - public ConsumesAttributeTests(MvcTestFixture fixture) + public ConsumesAttributeTests(MvcTestFixture fixture) : base(fixture) { } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs index 850ae97c7d..6e516407c1 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs @@ -16,9 +16,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class ContentNegotiationTest : IClassFixture> + public class ContentNegotiationTest : IClassFixture> { - public ContentNegotiationTest(MvcTestFixture fixture) + public ContentNegotiationTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/DefaultValuesTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/DefaultValuesTest.cs index d6aa6472a7..e57d8de17f 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/DefaultValuesTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/DefaultValuesTest.cs @@ -8,9 +8,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class DefaultValuesTest : IClassFixture> + public class DefaultValuesTest : IClassFixture> { - public DefaultValuesTest(MvcTestFixture fixture) + public DefaultValuesTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FiltersTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FiltersTest.cs index e3deb769d3..52bbef642b 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FiltersTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FiltersTest.cs @@ -9,9 +9,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class FiltersTest : IClassFixture> + public class FiltersTest : IClassFixture> { - public FiltersTest(MvcTestFixture fixture) + public FiltersTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/JsonResultTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/JsonResultTest.cs index f885d4ed28..43d442b945 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/JsonResultTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/JsonResultTest.cs @@ -8,9 +8,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class JsonResultTest : IClassFixture> + public class JsonResultTest : IClassFixture> { - public JsonResultTest(MvcTestFixture fixture) + public JsonResultTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/LinkGenerationTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/LinkGenerationTests.cs index f2328da15b..7b490c20db 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/LinkGenerationTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/LinkGenerationTests.cs @@ -12,7 +12,7 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class LinkGenerationTests : IClassFixture> + public class LinkGenerationTests : IClassFixture> { // Some tests require comparing the actual response body against an expected response baseline // so they require a reference to the assembly on which the resources are located, in order to @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // use it on all the rest of the tests. private static readonly Assembly _resourcesAssembly = typeof(LinkGenerationTests).GetTypeInfo().Assembly; - public LinkGenerationTests(MvcTestFixture fixture) + public LinkGenerationTests(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/OutputFormatterTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/OutputFormatterTest.cs index 41cf8029ac..54bdbf47dd 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/OutputFormatterTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/OutputFormatterTest.cs @@ -9,9 +9,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class OutputFormatterTest : IClassFixture> + public class OutputFormatterTest : IClassFixture> { - public OutputFormatterTest(MvcTestFixture fixture) + public OutputFormatterTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPageModelTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPageModelTest.cs index 66daab9783..ffd656ff62 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPageModelTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPageModelTest.cs @@ -11,16 +11,16 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RazorPageModelTest : IClassFixture> + public class RazorPageModelTest : IClassFixture> { - public RazorPageModelTest(MvcTestFixture fixture) + public RazorPageModelTest(MvcTestFixture fixture) { var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); Client = factory.CreateDefaultClient(); } private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => - builder.UseStartup(); + builder.UseStartup(); public HttpClient Client { get; } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesNamespaceTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesNamespaceTest.cs index f5260d9dca..7af6778323 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesNamespaceTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesNamespaceTest.cs @@ -9,16 +9,16 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RazorPagesNamespaceTest : IClassFixture> + public class RazorPagesNamespaceTest : IClassFixture> { - public RazorPagesNamespaceTest(MvcTestFixture fixture) + public RazorPagesNamespaceTest(MvcTestFixture fixture) { var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); Client = factory.CreateDefaultClient(); } private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => - builder.UseStartup(); + builder.UseStartup(); public HttpClient Client { get; } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 56e8785254..4fb1633dbf 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -18,18 +18,18 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RazorPagesTest : IClassFixture> + public class RazorPagesTest : IClassFixture> { private static readonly Assembly _resourcesAssembly = typeof(RazorPagesTest).GetTypeInfo().Assembly; - public RazorPagesTest(MvcTestFixture fixture) + public RazorPagesTest(MvcTestFixture fixture) { var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); Client = factory.CreateDefaultClient(); } private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => - builder.UseStartup(); + builder.UseStartup(); public HttpClient Client { get; } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesViewSearchTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesViewSearchTest.cs index a22742bf5d..f343aa65c3 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesViewSearchTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesViewSearchTest.cs @@ -9,16 +9,16 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RazorPagesViewSearchTest : IClassFixture> + public class RazorPagesViewSearchTest : IClassFixture> { - public RazorPagesViewSearchTest(MvcTestFixture fixture) + public RazorPagesViewSearchTest(MvcTestFixture fixture) { var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); Client = factory.CreateDefaultClient(); } private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => - builder.UseStartup(); + builder.UseStartup(); public HttpClient Client { get; } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs index 6e7be902ff..183372722d 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs @@ -8,9 +8,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RazorPagesWithEndpointRoutingTest : IClassFixture> + public class RazorPagesWithEndpointRoutingTest : IClassFixture> { - public RazorPagesWithEndpointRoutingTest(MvcTestFixture fixture) + public RazorPagesWithEndpointRoutingTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RemoteAttributeValidationTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RemoteAttributeValidationTest.cs index 8d5064bd49..30e9e96f7f 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RemoteAttributeValidationTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RemoteAttributeValidationTest.cs @@ -10,12 +10,12 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RemoteAttributeValidationTest : IClassFixture> + public class RemoteAttributeValidationTest : IClassFixture> { private static readonly Assembly _resourcesAssembly = typeof(RemoteAttributeValidationTest).GetTypeInfo().Assembly; - public RemoteAttributeValidationTest(MvcTestFixture fixture) + public RemoteAttributeValidationTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesEndpointRoutingTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesEndpointRoutingTest.cs index 113cfde4dc..e386705bc0 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesEndpointRoutingTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesEndpointRoutingTest.cs @@ -9,9 +9,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RequestServicesEndpointRoutingTest : RequestServicesTestBase + public class RequestServicesEndpointRoutingTest : RequestServicesTestBase { - public RequestServicesEndpointRoutingTest(MvcTestFixture fixture) + public RequestServicesEndpointRoutingTest(MvcTestFixture fixture) : base(fixture) { } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs index 73c0e5af15..31f2843fb4 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs @@ -8,9 +8,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RequestServicesTest : RequestServicesTestBase + public class RequestServicesTest : RequestServicesTestBase { - public RequestServicesTest(MvcTestFixture fixture) + public RequestServicesTest(MvcTestFixture fixture) : base(fixture) { } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingEndpointRoutingWithoutRazorPagesTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingEndpointRoutingWithoutRazorPagesTests.cs index 8b597e0ab8..1033e6cbe5 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingEndpointRoutingWithoutRazorPagesTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingEndpointRoutingWithoutRazorPagesTests.cs @@ -9,9 +9,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RoutingEndpointRoutingWithoutRazorPagesTests : RoutingWithoutRazorPagesTestsBase + public class RoutingEndpointRoutingWithoutRazorPagesTests : RoutingWithoutRazorPagesTestsBase { - public RoutingEndpointRoutingWithoutRazorPagesTests(MvcTestFixture fixture) + public RoutingEndpointRoutingWithoutRazorPagesTests(MvcTestFixture fixture) : base(fixture) { } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingUseMvcWithEndpointRoutingTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingUseMvcWithEndpointRoutingTest.cs new file mode 100644 index 0000000000..154a0fd965 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingUseMvcWithEndpointRoutingTest.cs @@ -0,0 +1,386 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class RoutingUseMvcWithEndpointRoutingTest : RoutingTestsBase + { + public RoutingUseMvcWithEndpointRoutingTest(MvcTestFixture fixture) + : base(fixture) + { + } + + [Fact] + public async Task AttributeRoutedAction_ContainsPage_RouteMatched() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/PageRoute/Attribute/pagevalue"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains("/PageRoute/Attribute/pagevalue", result.ExpectedUrls); + Assert.Equal("PageRoute", result.Controller); + Assert.Equal("AttributeRoute", result.Action); + + Assert.Contains( + new KeyValuePair("page", "pagevalue"), + result.RouteValues); + } + + [Fact] + public async Task ParameterTransformer_TokenReplacement_Found() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/parameter-transformer/my-action"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ParameterTransformer", result.Controller); + Assert.Equal("MyAction", result.Action); + } + + [Fact] + public async Task ParameterTransformer_TokenReplacement_NotFound() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ParameterTransformer/MyAction"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task AttributeRoutedAction_Parameters_Found() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/EndpointRouting/Index"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_Parameters_DefaultValue_Found() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/EndpointRouting"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_Found() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/endpoint-routing/ParameterTransformer"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_NotFound() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/EndpointRouting/ParameterTransformer"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkToSelf() + { + // Arrange + var url = LinkFrom("http://localhost/endpoint-routing/ParameterTransformer").To(new { }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/endpoint-routing/ParameterTransformer", result.Link); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkWithAmbientController() + { + // Arrange + var url = LinkFrom("http://localhost/endpoint-routing/ParameterTransformer").To(new { action = "Get", id = 5 }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/endpoint-routing/5", result.Link); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkToAttributeRoutedController() + { + // Arrange + var url = LinkFrom("http://localhost/endpoint-routing/ParameterTransformer").To(new { action = "ShowPosts", controller = "Blog" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/Blog/ShowPosts", result.Link); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkToConventionalController() + { + // Arrange + var url = LinkFrom("http://localhost/endpoint-routing/ParameterTransformer").To(new { action = "Index", controller = "Home" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/", result.Link); + } + + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.True(result); + } + + [Fact] + public async override Task RouteData_Routers_ConventionalRoute() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/RouteData/Conventional"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal( + Array.Empty(), + result.Routers); + } + + [Fact] + public async override Task RouteData_Routers_AttributeRoute() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/RouteData/Attribute"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal( + Array.Empty(), + result.Routers); + } + + // Endpoint routing exposes HTTP 405s for HTTP method mismatches + [Fact] + public override async Task ConventionalRoutedController_InArea_ActionBlockedByHttpMethod() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Travel/Flight/BuyTickets"); + + // Assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/conventional-transformer/Index"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_NotFound() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/ConventionalTransformer/Index"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_DefaultValue() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/conventional-transformer"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_WithParam() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/conventional-transformer/Param/my-value"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Param", result.Action); + + Assert.Equal("/ConventionalTransformerRoute/conventional-transformer/Param/my-value", Assert.Single(result.ExpectedUrls)); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_LinkToConventionalController() + { + // Arrange + var url = LinkFrom("http://localhost/ConventionalTransformerRoute/conventional-transformer/Index").To(new { action = "Index", controller = "Home" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + Assert.Equal("/", result.Link); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_LinkToConventionalControllerWithParam() + { + // Arrange + var url = LinkFrom("http://localhost/ConventionalTransformerRoute/conventional-transformer/Index").To(new { action = "Param", controller = "ConventionalTransformer", param = "MyValue" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + Assert.Equal("/ConventionalTransformerRoute/conventional-transformer/Param/my-value", result.Link); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_LinkToSelf() + { + // Arrange + var url = LinkFrom("http://localhost/ConventionalTransformerRoute/conventional-transformer/Index").To(new {}); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + Assert.Equal("/ConventionalTransformerRoute/conventional-transformer", result.Link); + } + + // Endpoint routing exposes HTTP 405s for HTTP method mismatches. + protected override void AssertCorsRejectionStatusCode(HttpResponseMessage response) + { + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingWithoutRazorPagesTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingWithoutRazorPagesTests.cs index 6e90048fc6..51ac1eb990 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingWithoutRazorPagesTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingWithoutRazorPagesTests.cs @@ -9,9 +9,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class RoutingWithoutRazorPagesTests : RoutingWithoutRazorPagesTestsBase + public class RoutingWithoutRazorPagesTests : RoutingWithoutRazorPagesTestsBase { - public RoutingWithoutRazorPagesTests(MvcTestFixture fixture) + public RoutingWithoutRazorPagesTests(MvcTestFixture fixture) : base(fixture) { } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInCookiesTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInCookiesTest.cs index b68cd88081..4e217bc025 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInCookiesTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInCookiesTest.cs @@ -14,9 +14,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class TempDataInCookiesTest : TempDataTestBase, IClassFixture> + public class TempDataInCookiesTest : TempDataTestBase, IClassFixture> { - public TempDataInCookiesTest(MvcTestFixture fixture) + public TempDataInCookiesTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataPropertyTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataPropertyTest.cs index 9899037d01..59744d2dbc 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataPropertyTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataPropertyTest.cs @@ -12,11 +12,11 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class TempDataPropertyTest : IClassFixture> + public class TempDataPropertyTest : IClassFixture> { protected HttpClient Client { get; } - public TempDataPropertyTest(MvcTestFixture fixture) + public TempDataPropertyTest(MvcTestFixture fixture) { Client = fixture.CreateDefaultClient(); } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureInheritanceTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureInheritanceTests.cs index 09a11fb4f8..2da8641640 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureInheritanceTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureInheritanceTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public void TestingInfrastructure_WebHost_WithWebHostBuilderRespectsCustomizations() { // Act - var factory = new CustomizedFactory(); + var factory = new CustomizedFactory(); var customized = factory .WithWebHostBuilder(builder => factory.ConfigureWebHostCalled.Add("Customization")) .WithWebHostBuilder(builder => factory.ConfigureWebHostCalled.Add("FurtherCustomization")); diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureTests.cs index 4efe202470..defdab27ca 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TestingInfrastructureTests.cs @@ -20,9 +20,9 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class TestingInfrastructureTests : IClassFixture> + public class TestingInfrastructureTests : IClassFixture> { - public TestingInfrastructureTests(WebApplicationFactory fixture) + public TestingInfrastructureTests(WebApplicationFactory fixture) { Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); Client = Factory.CreateClient(); @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.ConfigureTestServices(s => s.AddSingleton()); - public WebApplicationFactory Factory { get; } + public WebApplicationFactory Factory { get; } public HttpClient Client { get; } [Fact] diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionEndpointDataSourceTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionEndpointDataSourceTest.cs new file mode 100644 index 0000000000..d75b80a9f9 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionEndpointDataSourceTest.cs @@ -0,0 +1,123 @@ +// 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 Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class PageActionEndpointDataSourceTest : ActionEndpointDataSourceBaseTest + { + [Fact] + public void Endpoints_Ignores_NonPage() + { + // Arrange + var actions = new List + { + new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Test" }, + { "controller", "Test" }, + }, + }, + }; + + var mockDescriptorProvider = new Mock(); + mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0)); + + var dataSource = (PageActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + Assert.Empty(endpoints); + } + [Fact] + public void Endpoints_AppliesConventions() + { + // Arrange + var actions = new List + { + new PageActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Test" }, + { "controller", "Test" }, + }, + }, + }; + + var mockDescriptorProvider = new Mock(); + mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0)); + + var dataSource = (PageActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object); + + dataSource.Add((b) => + { + b.Metadata.Add("Hi there"); + }); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + Assert.Collection( + endpoints.OfType().OrderBy(e => e.RoutePattern.RawText), + e => + { + Assert.Equal("/test", e.RoutePattern.RawText); + Assert.Same(actions[0], e.Metadata.GetMetadata()); + Assert.Equal("Hi there", e.Metadata.GetMetadata()); + }); + } + + private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory) + { + return new PageActionEndpointDataSource(actions, endpointFactory); + } + + protected override ActionDescriptor CreateActionDescriptor( + object values, + string pattern = null, + IList metadata = null) + { + var action = new PageActionDescriptor(); + + foreach (var kvp in new RouteValueDictionary(values)) + { + action.RouteValues[kvp.Key] = kvp.Value?.ToString(); + } + + if (!string.IsNullOrEmpty(pattern)) + { + action.AttributeRouteInfo = new AttributeRouteInfo + { + Name = "test", + Template = pattern, + }; + } + + action.EndpointMetadata = metadata; + return action; + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj b/src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj index 40cce2ed86..0c21b5b6f8 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj @@ -4,6 +4,10 @@ netcoreapp3.0 + + + + diff --git a/src/Mvc/test/WebSites/ApiExplorerWebSite/Startup.cs b/src/Mvc/test/WebSites/ApiExplorerWebSite/Startup.cs index 28d1b4d6cf..571218280f 100644 --- a/src/Mvc/test/WebSites/ApiExplorerWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/ApiExplorerWebSite/Startup.cs @@ -46,9 +46,9 @@ namespace ApiExplorerWebSite public void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute("default", "{controller}/{action}"); + routes.MapDefaultControllerRoute(); }); } diff --git a/src/Mvc/test/WebSites/ApplicationModelWebSite/Startup.cs b/src/Mvc/test/WebSites/ApplicationModelWebSite/Startup.cs index 899b3c13fa..93041038b0 100644 --- a/src/Mvc/test/WebSites/ApplicationModelWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/ApplicationModelWebSite/Startup.cs @@ -27,14 +27,12 @@ namespace ApplicationModelWebSite public void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute(name: "areaRoute", - template: "{area:exists}/{controller=Home}/{action=Index}"); + routes.MapControllerRoute(name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}"); + routes.MapControllerRoute(name: "default", template: "{controller}/{action}/{id?}"); - routes.MapRoute( - name: "default", - template: "{controller}/{action}/{id?}"); + routes.MapRazorPages(); }); } diff --git a/src/Mvc/test/WebSites/BasicWebSite/Program.cs b/src/Mvc/test/WebSites/BasicWebSite/Program.cs index 11818853eb..88a390d422 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/Program.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/Program.cs @@ -16,7 +16,7 @@ namespace BasicWebSite public static IWebHostBuilder CreateWebHostBuilder(string[] args) => new WebHostBuilder() .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup() + .UseStartup() .UseKestrel() .UseIISIntegration(); } diff --git a/src/Mvc/test/WebSites/BasicWebSite/Startup.cs b/src/Mvc/test/WebSites/BasicWebSite/Startup.cs index 978ce38fe7..dc49599f62 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/Startup.cs @@ -1,18 +1,10 @@ // 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.Net.Http; -using BasicWebSite.Services; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace BasicWebSite { @@ -21,77 +13,38 @@ namespace BasicWebSite // Set up application services public void ConfigureServices(IServiceCollection services) { - services.AddSingleton(new TestService { Message = "true" }); + services.AddRouting(); - services.AddAuthentication() - .AddScheme("Api", _ => { }); - services.AddTransient(); - - services - .AddMvc(options => - { - options.Conventions.Add(new ApplicationDescription("This is a basic website.")); - // Filter that records a value in HttpContext.Items - options.Filters.Add(new TraceResourceFilter()); - - // Remove when all URL generation tests are passing - https://github.com/aspnet/Routing/issues/590 - options.EnableEndpointRouting = false; - }) + services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Latest) .AddNewtonsoftJson() .AddXmlDataContractSerializerFormatters(); services.ConfigureBaseWebSiteAuthPolicies(); - services.AddTransient(); - - services.AddLogging(); - services.AddSingleton(); services.AddHttpContextAccessor(); - services.AddSingleton(); services.AddScoped(); - services.AddTransient(); services.AddScoped(); services.AddSingleton(); - services.TryAddSingleton(CreateWeatherForecastService); - } - - // For manual debug only (running this test site with F5) - // This needs to be changed to match the site host - private WeatherForecastService CreateWeatherForecastService(IServiceProvider serviceProvider) - { - var contextAccessor = serviceProvider.GetRequiredService(); - var httpContext = contextAccessor.HttpContext; - if (httpContext == null) - { - throw new InvalidOperationException("Needs a request context!"); - } - var client = new HttpClient(); - client.BaseAddress = new Uri($"{httpContext.Request.Scheme}://{httpContext.Request.Host}"); - return new WeatherForecastService(client); } public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); - app.UseStaticFiles(); - // Initializes the RequestId service for each request app.UseMiddleware(); - // Add MVC to the request pipeline - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute( - "areaRoute", - "{area:exists}/{controller}/{action}", - new { controller = "Home", action = "Index" }); - - routes.MapRoute("ActionAsMethod", "{controller}/{action}", + routes.MapControllerRoute( + name: "ActionAsMethod", + template: "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); - routes.MapRoute("PageRoute", "{controller}/{action}/{page}"); + routes.MapControllerRoute( + name: "PageRoute", + template: "{controller}/{action}/{page}"); }); } } diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs index 72aff81040..286169d54d 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs @@ -37,7 +37,11 @@ namespace BasicWebSite return next(); }); - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + routes.MapRazorPages(); + }); } private class RequestBodySizeCheckingStream : Stream diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCookieTempDataProviderAndCookieConsent.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCookieTempDataProviderAndCookieConsent.cs index 923109fdda..25abeeca3b 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCookieTempDataProviderAndCookieConsent.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCookieTempDataProviderAndCookieConsent.cs @@ -29,7 +29,11 @@ namespace BasicWebSite app.UseCookiePolicy(); - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + routes.MapRazorPages(); + }); } } } diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs index f7d688a77a..f37bc908d4 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs @@ -47,7 +47,11 @@ namespace BasicWebSite public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); - app.UseMvc(); + + app.UseRouting(routes => + { + routes.MapControllers(); + }); } } } diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithEndpointRouting.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithEndpointRouting.cs deleted file mode 100644 index 97c93de73a..0000000000 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupWithEndpointRouting.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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 Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.DependencyInjection; - -namespace BasicWebSite -{ - public class StartupWithEndpointRouting - { - // Set up application services - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - - services.AddMvc() - .SetCompatibilityVersion(CompatibilityVersion.Latest) - .AddNewtonsoftJson() - .AddXmlDataContractSerializerFormatters(); - - services.ConfigureBaseWebSiteAuthPolicies(); - - services.AddHttpContextAccessor(); - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - } - - public void Configure(IApplicationBuilder app) - { - app.UseDeveloperExceptionPage(); - - // Initializes the RequestId service for each request - app.UseMiddleware(); - - app.UseMvc(routes => - { - routes.MapRoute( - "ActionAsMethod", - "{controller}/{action}", - defaults: new { controller = "Home", action = "Index" }); - - routes.MapRoute("PageRoute", "{controller}/{action}/{page}"); - }); - } - } -} diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithSessionTempDataProvider.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithSessionTempDataProvider.cs index 1d868bc638..9c321e2001 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupWithSessionTempDataProvider.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithSessionTempDataProvider.cs @@ -25,7 +25,12 @@ namespace BasicWebSite { app.UseDeveloperExceptionPage(); app.UseSession(); - app.UseMvcWithDefaultRoute(); + + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + routes.MapRazorPages(); + }); } } } diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithoutEndpointRouting.cs new file mode 100644 index 0000000000..16828aacd3 --- /dev/null +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithoutEndpointRouting.cs @@ -0,0 +1,97 @@ +// 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.Net.Http; +using BasicWebSite.Services; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace BasicWebSite +{ + public class StartupWithoutEndpointRouting + { + // Set up application services + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(new TestService { Message = "true" }); + + services.AddAuthentication() + .AddScheme("Api", _ => { }); + services.AddTransient(); + + services + .AddMvc(options => + { + options.Conventions.Add(new ApplicationDescription("This is a basic website.")); + // Filter that records a value in HttpContext.Items + options.Filters.Add(new TraceResourceFilter()); + + options.EnableEndpointRouting = false; + }) + .SetCompatibilityVersion(CompatibilityVersion.Latest) + .AddNewtonsoftJson() + .AddXmlDataContractSerializerFormatters(); + + services.ConfigureBaseWebSiteAuthPolicies(); + + services.AddTransient(); + + services.AddLogging(); + services.AddSingleton(); + services.AddHttpContextAccessor(); + services.AddSingleton(); + services.AddScoped(); + services.AddTransient(); + services.AddScoped(); + services.AddSingleton(); + services.TryAddSingleton(CreateWeatherForecastService); + } + + // For manual debug only (running this test site with F5) + // This needs to be changed to match the site host + private WeatherForecastService CreateWeatherForecastService(IServiceProvider serviceProvider) + { + var contextAccessor = serviceProvider.GetRequiredService(); + var httpContext = contextAccessor.HttpContext; + if (httpContext == null) + { + throw new InvalidOperationException("Needs a request context!"); + } + var client = new HttpClient(); + client.BaseAddress = new Uri($"{httpContext.Request.Scheme}://{httpContext.Request.Host}"); + return new WeatherForecastService(client); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseStaticFiles(); + + // Initializes the RequestId service for each request + app.UseMiddleware(); + + // Add MVC to the request pipeline + app.UseMvc(routes => + { + routes.MapRoute( + "areaRoute", + "{area:exists}/{controller}/{action}", + new { controller = "Home", action = "Index" }); + + routes.MapRoute("ActionAsMethod", "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }); + + routes.MapRoute("PageRoute", "{controller}/{action}/{page}"); + }); + } + } +} diff --git a/src/Mvc/test/WebSites/ControllersFromServicesWebSite/Startup.cs b/src/Mvc/test/WebSites/ControllersFromServicesWebSite/Startup.cs index ac25531280..f8e7b9dd7f 100644 --- a/src/Mvc/test/WebSites/ControllersFromServicesWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/ControllersFromServicesWebSite/Startup.cs @@ -64,9 +64,9 @@ namespace ControllersFromServicesWebSite public void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute("default", "{controller}/{action}/{id}"); + routes.MapDefaultControllerRoute(); }); } diff --git a/src/Mvc/test/WebSites/CorsWebSite/Startup.cs b/src/Mvc/test/WebSites/CorsWebSite/Startup.cs index 37012118da..62eca5374f 100644 --- a/src/Mvc/test/WebSites/CorsWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/CorsWebSite/Startup.cs @@ -73,9 +73,12 @@ namespace CorsWebSite }); } - public void Configure(IApplicationBuilder app) + public virtual void Configure(IApplicationBuilder app) { - app.UseMvc(); + app.UseRouting(routes => + { + routes.MapControllers(); + }); } protected virtual void ConfigureMvcOptions(MvcOptions options) diff --git a/src/Mvc/test/WebSites/CorsWebSite/StartupWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/CorsWebSite/StartupWithoutEndpointRouting.cs index 308535f451..0ab34118f0 100644 --- a/src/Mvc/test/WebSites/CorsWebSite/StartupWithoutEndpointRouting.cs +++ b/src/Mvc/test/WebSites/CorsWebSite/StartupWithoutEndpointRouting.cs @@ -1,12 +1,18 @@ // 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; namespace CorsWebSite { public class StartupWithoutEndpointRouting : Startup { + public override void Configure(IApplicationBuilder app) + { + app.UseMvc(); + } + protected override void ConfigureMvcOptions(MvcOptions options) { options.EnableEndpointRouting = false; diff --git a/src/Mvc/test/WebSites/ErrorPageMiddlewareWebSite/Startup.cs b/src/Mvc/test/WebSites/ErrorPageMiddlewareWebSite/Startup.cs index 3fc459a06f..2a9c4b0b21 100644 --- a/src/Mvc/test/WebSites/ErrorPageMiddlewareWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/ErrorPageMiddlewareWebSite/Startup.cs @@ -21,7 +21,10 @@ namespace ErrorPageMiddlewareWebSite public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); - app.UseMvc(); + app.UseRouting(routes => + { + routes.MapControllers(); + }); } public static void Main(string[] args) diff --git a/src/Mvc/test/WebSites/FilesWebSite/Startup.cs b/src/Mvc/test/WebSites/FilesWebSite/Startup.cs index 8bb9762577..d3821e06d8 100644 --- a/src/Mvc/test/WebSites/FilesWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/FilesWebSite/Startup.cs @@ -21,9 +21,9 @@ namespace FilesWebSite public void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute(name: null, template: "{controller}/{action}", defaults: null); + routes.MapDefaultControllerRoute(); }); } diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Startup.cs b/src/Mvc/test/WebSites/FormatterWebSite/Startup.cs index 82115f7844..796ddc3037 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/Startup.cs @@ -26,10 +26,9 @@ namespace FormatterWebSite public void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute("ActionAsMethod", "{controller}/{action}", - defaults: new { controller = "Home", action = "Index" }); + routes.MapDefaultControllerRoute(); }); } } diff --git a/src/Mvc/test/WebSites/FormatterWebSite/StartupWithRespectBrowserAcceptHeader.cs b/src/Mvc/test/WebSites/FormatterWebSite/StartupWithRespectBrowserAcceptHeader.cs index 1a31461800..ee419a2caf 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/StartupWithRespectBrowserAcceptHeader.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/StartupWithRespectBrowserAcceptHeader.cs @@ -19,10 +19,9 @@ namespace FormatterWebSite public void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute("ActionAsMethod", "{controller}/{action}", - defaults: new { controller = "Home", action = "Index" }); + routes.MapDefaultControllerRoute(); }); } } diff --git a/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs b/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs index 7250470c07..adb6da9a44 100644 --- a/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs @@ -40,18 +40,17 @@ namespace GenericHostWebSite app.UseStaticFiles(); - // Add MVC to the request pipeline - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute( + routes.MapControllerRoute( "areaRoute", "{area:exists}/{controller}/{action}", new { controller = "Home", action = "Index" }); - routes.MapRoute("ActionAsMethod", "{controller}/{action}", + routes.MapControllerRoute("ActionAsMethod", "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); - routes.MapRoute("PageRoute", "{controller}/{action}/{page}"); + routes.MapControllerRoute("PageRoute", "{controller}/{action}/{page}"); }); } } diff --git a/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs b/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs index 3e04fa49f7..e3674aeed8 100644 --- a/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs @@ -26,23 +26,26 @@ namespace HtmlGenerationWebSite services.AddSingleton(); } - public void Configure(IApplicationBuilder app) + public virtual void Configure(IApplicationBuilder app) { app.UseStaticFiles(); - app.UseMvc(routes => + + app.UseRouting(routes => { - routes.MapRoute( + routes.MapControllerRoute( name: "areaRoute", template: "{area:exists}/{controller}/{action}/{id?}", defaults: new { action = "Index" }); - routes.MapRoute( + routes.MapControllerRoute( name: "productRoute", template: "Product/{action}", defaults: new { controller = "Product" }); - routes.MapRoute( + routes.MapControllerRoute( name: "default", template: "{controller}/{action}/{id?}", defaults: new { controller = "HtmlGeneration_Home", action = "Index" }); + + routes.MapRazorPages(); }); } diff --git a/src/Mvc/test/WebSites/HtmlGenerationWebSite/StartupWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/HtmlGenerationWebSite/StartupWithoutEndpointRouting.cs index df0f387a9b..6a680e0020 100644 --- a/src/Mvc/test/WebSites/HtmlGenerationWebSite/StartupWithoutEndpointRouting.cs +++ b/src/Mvc/test/WebSites/HtmlGenerationWebSite/StartupWithoutEndpointRouting.cs @@ -1,12 +1,34 @@ // 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; namespace HtmlGenerationWebSite { public class StartupWithoutEndpointRouting : Startup { + public override void Configure(IApplicationBuilder app) + { + app.UseStaticFiles(); + + app.UseMvc(routes => + { + routes.MapRoute( + name: "areaRoute", + template: "{area:exists}/{controller}/{action}/{id?}", + defaults: new { action = "Index" }); + routes.MapRoute( + name: "productRoute", + template: "Product/{action}", + defaults: new { controller = "Product" }); + routes.MapRoute( + name: "default", + template: "{controller}/{action}/{id?}", + defaults: new { controller = "HtmlGeneration_Home", action = "Index" }); + }); + } + protected override void ConfigureMvcOptions(MvcOptions options) { options.EnableEndpointRouting = false; diff --git a/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs b/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs index 5d5123ee9d..b6dcfb42b7 100644 --- a/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs @@ -23,7 +23,11 @@ namespace RazorBuildWebSite public void Configure(IApplicationBuilder app) { - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + routes.MapRazorPages(); + }); } public static void Main(string[] args) diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Startup.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Startup.cs index 5b6a6c7fec..bf0fa2fa99 100644 --- a/src/Mvc/test/WebSites/RazorPagesWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Startup.cs @@ -1,12 +1,10 @@ // 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.Globalization; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using RazorPagesWebSite.Conventions; namespace RazorPagesWebSite { @@ -14,42 +12,26 @@ namespace RazorPagesWebSite { public void ConfigureServices(IServiceCollection services) { - services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => options.LoginPath = "/Login"); + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => options.LoginPath = "/Login"); + services.AddMvc() - .AddMvcLocalization() - .AddNewtonsoftJson() .AddRazorPagesOptions(options => { - options.Conventions.AuthorizePage("/HelloWorldWithAuth"); - options.Conventions.AuthorizeFolder("/Pages/Admin"); - options.Conventions.AllowAnonymousToPage("/Pages/Admin/Login"); - options.Conventions.AddPageRoute("/HelloWorldWithRoute", "Different-Route/{text}"); - options.Conventions.AddPageRoute("/Pages/NotTheRoot", string.Empty); - options.Conventions.Add(new CustomModelTypeConvention()); + options.Conventions.AuthorizeFolder("/Admin"); }) - .WithRazorPagesAtContentRoot() .SetCompatibilityVersion(CompatibilityVersion.Latest); } public void Configure(IApplicationBuilder app) { - app.UseAuthentication(); - - app.UseStaticFiles(); - - var supportedCultures = new[] + app.UseRouting(routes => { - new CultureInfo("en-US"), - new CultureInfo("fr-FR"), - }; - - app.UseRequestLocalization(new RequestLocalizationOptions - { - SupportedCultures = supportedCultures, - SupportedUICultures = supportedCultures + routes.MapControllers(); + routes.MapRazorPages(); }); - app.UseMvc(); + app.UseAuthorization(); } } } diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs index bf0b036274..e6652b4818 100644 --- a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs @@ -42,9 +42,10 @@ namespace RazorPagesWebSite app.UseStaticFiles(); - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute("areaRoute", "{area:exists}/{controller=Home}/{action=Index}"); + routes.MapControllerRoute("areaRoute", "{area:exists}/{controller=Home}/{action=Index}"); + routes.MapRazorPages(); }); } } diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithEndpointRouting.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithEndpointRouting.cs deleted file mode 100644 index 3483ad0b0f..0000000000 --- a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithEndpointRouting.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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 Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; - -namespace RazorPagesWebSite -{ - public class StartupWithEndpointRouting - { - public void ConfigureServices(IServiceCollection services) - { - services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(options => options.LoginPath = "/Login"); - - services.AddMvc() - .AddRazorPagesOptions(options => - { - options.Conventions.AuthorizeFolder("/Admin"); - }) - .SetCompatibilityVersion(CompatibilityVersion.Latest); - } - - public void Configure(IApplicationBuilder app) - { - app.UseRouting(routes => - { - routes.MapApplication(); - }); - - app.UseAuthorization(); - } - } -} diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs new file mode 100644 index 0000000000..7cd47c7a65 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs @@ -0,0 +1,55 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using RazorPagesWebSite.Conventions; + +namespace RazorPagesWebSite +{ + public class StartupWithoutEndpointRouting + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => options.LoginPath = "/Login"); + services.AddMvc() + .AddMvcLocalization() + .AddNewtonsoftJson() + .AddRazorPagesOptions(options => + { + options.Conventions.AuthorizePage("/HelloWorldWithAuth"); + options.Conventions.AuthorizeFolder("/Pages/Admin"); + options.Conventions.AllowAnonymousToPage("/Pages/Admin/Login"); + options.Conventions.AddPageRoute("/HelloWorldWithRoute", "Different-Route/{text}"); + options.Conventions.AddPageRoute("/Pages/NotTheRoot", string.Empty); + options.Conventions.Add(new CustomModelTypeConvention()); + }) + .WithRazorPagesAtContentRoot() + .SetCompatibilityVersion(CompatibilityVersion.Latest); + } + + public void Configure(IApplicationBuilder app) + { + app.UseAuthentication(); + + app.UseStaticFiles(); + + var supportedCultures = new[] + { + new CultureInfo("en-US"), + new CultureInfo("fr-FR"), + }; + + app.UseRequestLocalization(new RequestLocalizationOptions + { + SupportedCultures = supportedCultures, + SupportedUICultures = supportedCultures + }); + + app.UseMvc(); + } + } +} diff --git a/src/Mvc/test/WebSites/RazorWebSite/Startup.cs b/src/Mvc/test/WebSites/RazorWebSite/Startup.cs index f49df81d55..6f7471b33c 100644 --- a/src/Mvc/test/WebSites/RazorWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/RazorWebSite/Startup.cs @@ -62,7 +62,11 @@ namespace RazorWebSite } }); - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + routes.MapRazorPages(); + }); } } } diff --git a/src/Mvc/test/WebSites/RazorWebSite/StartupDataAnnotations.cs b/src/Mvc/test/WebSites/RazorWebSite/StartupDataAnnotations.cs index d3159ea501..5f3ae08f90 100644 --- a/src/Mvc/test/WebSites/RazorWebSite/StartupDataAnnotations.cs +++ b/src/Mvc/test/WebSites/RazorWebSite/StartupDataAnnotations.cs @@ -45,9 +45,11 @@ namespace RazorWebSite } }); app.UseStaticFiles(); - - // Add MVC to the request pipeline - app.UseMvcWithDefaultRoute(); + + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + }); } } } diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs b/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs index ce27bce2a2..47a966efd7 100644 --- a/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace RoutingWebSite @@ -37,54 +36,64 @@ namespace RoutingWebSite services.AddSingleton(); } - public void Configure(IApplicationBuilder app) + public virtual void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute( + routes.MapControllerRoute( "NonParameterConstraintRoute", "NonParameterConstraintRoute/{controller}/{action}", defaults: null, constraints: new { controller = "NonParameterConstraint", nonParameter = new QueryStringConstraint() }); - routes.MapRoute( + routes.MapControllerRoute( "DataTokensRoute", "DataTokensRoute/{controller}/{action}", defaults: null, constraints: new { controller = "DataTokens" }, dataTokens: new { hasDataTokens = true }); - ConfigureConventionalTransformerRoute(routes); + routes.MapControllerRoute( + "ConventionalTransformerRoute", + "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", + defaults: null, + constraints: new { controller = "ConventionalTransformer" }); - routes.MapRoute( + routes.MapControllerRoute( "DefaultValuesRoute_OptionalParameter", "DefaultValuesRoute/Optional/{controller=DEFAULTVALUES}/{action=OPTIONALPARAMETER}/{id?}/{**catchAll}", defaults: null, constraints: new { controller = "DefaultValues", action = "OptionalParameter" }); - routes.MapRoute( + routes.MapControllerRoute( "DefaultValuesRoute_DefaultParameter", "DefaultValuesRoute/Default/{controller=DEFAULTVALUES}/{action=DEFAULTPARAMETER}/{id=17}/{**catchAll}", defaults: null, constraints: new { controller = "DefaultValues", action = "DefaultParameter" }); - routes.MapAreaRoute( + routes.MapAreaControllerRoute( "flightRoute", "adminRoute", "{area:exists}/{controller}/{action}", defaults: new { controller = "Home", action = "Index" }, constraints: new { area = "Travel" }); - ConfigurePageRoute(routes); + routes.MapControllerRoute( + "PageRoute", + "{controller}/{action}/{page}", + defaults: null, + constraints: new { controller = "PageRoute" }); - routes.MapRoute( + routes.MapControllerRoute( "ActionAsMethod", "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); - routes.MapRoute( + routes.MapControllerRoute( "RouteWithOptionalSegment", "{controller}/{action}/{path?}"); + + routes.MapRazorPages(); }); app.Map("/afterrouting", b => b.Run(c => @@ -105,23 +114,5 @@ namespace RoutingWebSite { services.AddRouting(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer)); } - - protected virtual void ConfigureConventionalTransformerRoute(IRouteBuilder routes) - { - routes.MapRoute( - "ConventionalTransformerRoute", - "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", - defaults: null, - constraints: new { controller = "ConventionalTransformer" }); - } - - protected virtual void ConfigurePageRoute(IRouteBuilder routes) - { - routes.MapRoute( - "PageRoute", - "{controller}/{action}/{page}", - defaults: null, - constraints: new { controller = "PageRoute" }); - } } } diff --git a/src/Mvc/test/WebSites/RoutingWebSite/StartupForLinkGenerator.cs b/src/Mvc/test/WebSites/RoutingWebSite/StartupForLinkGenerator.cs index 557bc03053..fee063c20d 100644 --- a/src/Mvc/test/WebSites/RoutingWebSite/StartupForLinkGenerator.cs +++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupForLinkGenerator.cs @@ -39,7 +39,11 @@ namespace RoutingWebSite public void Configure(IApplicationBuilder app) { - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + routes.MapRazorPages(); + }); } } } \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RoutingWebSite/StartupWithUseMvcAndEndpointRouting.cs b/src/Mvc/test/WebSites/RoutingWebSite/StartupWithUseMvcAndEndpointRouting.cs new file mode 100644 index 0000000000..6d90c870f9 --- /dev/null +++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupWithUseMvcAndEndpointRouting.cs @@ -0,0 +1,80 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace RoutingWebSite +{ + public class StartupWithUseMvcAndEndpointRouting : Startup + { + public override void Configure(IApplicationBuilder app) + { + app.UseMvc(routes => + { + routes.MapRoute( + "NonParameterConstraintRoute", + "NonParameterConstraintRoute/{controller}/{action}", + defaults: null, + constraints: new { controller = "NonParameterConstraint", nonParameter = new QueryStringConstraint() }); + + routes.MapRoute( + "DataTokensRoute", + "DataTokensRoute/{controller}/{action}", + defaults: null, + constraints: new { controller = "DataTokens" }, + dataTokens: new { hasDataTokens = true }); + + routes.MapRoute( + "ConventionalTransformerRoute", + "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", + defaults: null, + constraints: new { controller = "ConventionalTransformer" }); + + routes.MapRoute( + "DefaultValuesRoute_OptionalParameter", + "DefaultValuesRoute/Optional/{controller=DEFAULTVALUES}/{action=OPTIONALPARAMETER}/{id?}/{**catchAll}", + defaults: null, + constraints: new { controller = "DefaultValues", action = "OptionalParameter" }); + + routes.MapRoute( + "DefaultValuesRoute_DefaultParameter", + "DefaultValuesRoute/Default/{controller=DEFAULTVALUES}/{action=DEFAULTPARAMETER}/{id=17}/{**catchAll}", + defaults: null, + constraints: new { controller = "DefaultValues", action = "DefaultParameter" }); + + routes.MapAreaRoute( + "flightRoute", + "adminRoute", + "{area:exists}/{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }, + constraints: new { area = "Travel" }); + + routes.MapRoute( + "PageRoute", + "{controller}/{action}/{page}", + defaults: null, + constraints: new { controller = "PageRoute" }); + + routes.MapRoute( + "ActionAsMethod", + "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }); + + routes.MapRoute( + "RouteWithOptionalSegment", + "{controller}/{action}/{path?}"); + }); + + app.Map("/afterrouting", b => b.Run(c => + { + return c.Response.WriteAsync("Hello from middleware after routing"); + })); + } + } +} diff --git a/src/Mvc/test/WebSites/RoutingWebSite/StartupWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/RoutingWebSite/StartupWithoutEndpointRouting.cs index 083573b622..9b74128a82 100644 --- a/src/Mvc/test/WebSites/RoutingWebSite/StartupWithoutEndpointRouting.cs +++ b/src/Mvc/test/WebSites/RoutingWebSite/StartupWithoutEndpointRouting.cs @@ -1,6 +1,8 @@ // 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Routing; @@ -11,6 +13,58 @@ namespace RoutingWebSite { public class StartupWithoutEndpointRouting : Startup { + public override void Configure(IApplicationBuilder app) + { + app.UseMvc(routes => + { + routes.MapRoute( + "NonParameterConstraintRoute", + "NonParameterConstraintRoute/{controller}/{action}", + defaults: null, + constraints: new { controller = "NonParameterConstraint", nonParameter = new QueryStringConstraint() }); + + routes.MapRoute( + "DataTokensRoute", + "DataTokensRoute/{controller}/{action}", + defaults: null, + constraints: new { controller = "DataTokens" }, + dataTokens: new { hasDataTokens = true }); + + routes.MapRoute( + "DefaultValuesRoute_OptionalParameter", + "DefaultValuesRoute/Optional/{controller=DEFAULTVALUES}/{action=OPTIONALPARAMETER}/{id?}/{**catchAll}", + defaults: null, + constraints: new { controller = "DefaultValues", action = "OptionalParameter" }); + + routes.MapRoute( + "DefaultValuesRoute_DefaultParameter", + "DefaultValuesRoute/Default/{controller=DEFAULTVALUES}/{action=DEFAULTPARAMETER}/{id=17}/{**catchAll}", + defaults: null, + constraints: new { controller = "DefaultValues", action = "DefaultParameter" }); + + routes.MapAreaRoute( + "flightRoute", + "adminRoute", + "{area:exists}/{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }, + constraints: new { area = "Travel" }); + + routes.MapRoute( + "ActionAsMethod", + "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }); + + routes.MapRoute( + "RouteWithOptionalSegment", + "{controller}/{action}/{path?}"); + }); + + app.Map("/afterrouting", b => b.Run(c => + { + return c.Response.WriteAsync("Hello from middleware after routing"); + })); + } + // Do not call base implementations of these methods. Those are specific to endpoint routing. protected override void ConfigureMvcOptions(MvcOptions options) { @@ -35,15 +89,5 @@ namespace RoutingWebSite services.TryAddEnumerable(ServiceDescriptor.Singleton(actionDescriptorProvider)); } - - protected override void ConfigureConventionalTransformerRoute(IRouteBuilder routes) - { - // no-op - } - - protected override void ConfigurePageRoute(IRouteBuilder routes) - { - // no-op - } } } diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs b/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs index 08db0bdd8d..16d9364aea 100644 --- a/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs @@ -32,11 +32,9 @@ namespace SecurityWebSite { app.UseAuthentication(); - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); + routes.MapDefaultControllerRoute(); }); } } diff --git a/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilter.cs b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilter.cs index 01c796512f..ca1167e13a 100644 --- a/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilter.cs +++ b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilter.cs @@ -35,7 +35,10 @@ namespace SecurityWebSite { app.UseAuthentication(); - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + }); } } } diff --git a/src/Mvc/test/WebSites/SimpleWebSite/Startup.cs b/src/Mvc/test/WebSites/SimpleWebSite/Startup.cs index 1757a7adc3..8de6a98153 100644 --- a/src/Mvc/test/WebSites/SimpleWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/SimpleWebSite/Startup.cs @@ -26,7 +26,10 @@ namespace SimpleWebSite public void Configure(IApplicationBuilder app) { - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + }); } public static void Main(string[] args) diff --git a/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs b/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs index 600b2b6c62..0caf7375b9 100644 --- a/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/TagHelpersWebSite/Startup.cs @@ -20,7 +20,10 @@ namespace TagHelpersWebSite public void Configure(IApplicationBuilder app) { - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + }); } public static void Main(string[] args) diff --git a/src/Mvc/test/WebSites/VersioningWebSite/Startup.cs b/src/Mvc/test/WebSites/VersioningWebSite/Startup.cs index c7031ec301..909af127d7 100644 --- a/src/Mvc/test/WebSites/VersioningWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/VersioningWebSite/Startup.cs @@ -21,9 +21,12 @@ namespace VersioningWebSite services.AddSingleton(); } - public void Configure(IApplicationBuilder app) + public virtual void Configure(IApplicationBuilder app) { - app.UseMvcWithDefaultRoute(); + app.UseRouting(routes => + { + routes.MapDefaultControllerRoute(); + }); } protected virtual void ConfigureMvcOptions(MvcOptions options) diff --git a/src/Mvc/test/WebSites/VersioningWebSite/StartupWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/VersioningWebSite/StartupWithoutEndpointRouting.cs index 0b1668384c..c6fc0c475a 100644 --- a/src/Mvc/test/WebSites/VersioningWebSite/StartupWithoutEndpointRouting.cs +++ b/src/Mvc/test/WebSites/VersioningWebSite/StartupWithoutEndpointRouting.cs @@ -1,12 +1,18 @@ // 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; namespace VersioningWebSite { public class StartupWithoutEndpointRouting : Startup { + public override void Configure(IApplicationBuilder app) + { + app.UseMvcWithDefaultRoute(); + } + protected override void ConfigureMvcOptions(MvcOptions options) { options.EnableEndpointRouting = false; diff --git a/src/Mvc/test/WebSites/XmlFormattersWebSite/Startup.cs b/src/Mvc/test/WebSites/XmlFormattersWebSite/Startup.cs index 03cd9d9bd2..74c2e21b5a 100644 --- a/src/Mvc/test/WebSites/XmlFormattersWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/XmlFormattersWebSite/Startup.cs @@ -107,10 +107,9 @@ namespace XmlFormattersWebSite public void Configure(IApplicationBuilder app) { - app.UseMvc(routes => + app.UseRouting(routes => { - routes.MapRoute("ActionAsMethod", "{controller}/{action}", - defaults: new { controller = "Home", action = "Index" }); + routes.MapDefaultControllerRoute(); }); } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs index c31f05f8d8..bcb0e16987 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs @@ -160,7 +160,7 @@ namespace Company.WebApplication1 app.UseRouting(routes => { - routes.MapApplication(); + routes.MapRazorPages(); }); app.UseCookiePolicy(); diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs index ad015614d4..8c14404309 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs @@ -159,10 +159,10 @@ namespace Company.WebApplication1 app.UseRouting(routes => { - routes.MapApplication(); routes.MapControllerRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); + routes.MapRazorPages(); }); app.UseCookiePolicy(); diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs index fec0a8daf5..b18e598bf2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/Startup.fs @@ -41,11 +41,11 @@ type Startup private () = app.UseStaticFiles() |> ignore app.UseRouting(fun routes -> - routes.MapApplication() |> ignore routes.MapControllerRoute( name = "default", template = "{controller=Home}/{action=Index}/{id?}") |> ignore ) |> ignore + routes.MapRazorPages() |> ignore app.UseAuthorization() |> ignore diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs index 9a4aa1a8a7..92dbf02d54 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs @@ -66,7 +66,7 @@ namespace Company.WebApplication1 app.UseRouting(routes => { - routes.MapApplication(); + routes.MapControllers(); }); #if (OrganizationalAuth || IndividualAuth) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs index 04885c1d40..03dc129e28 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/Startup.fs @@ -37,7 +37,7 @@ type Startup private () = #endif app.UseRouting(fun routes -> - routes.MapApplication() |> ignore + routes.MapControllers() |> ignore ) |> ignore app.UseAuthorization() |> ignore