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