diff --git a/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs b/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs index efdb91d2f6..93e78e9094 100644 --- a/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs +++ b/src/Mvc/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs @@ -58,6 +58,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance { var endpointDataSource = CreateMvcEndpointDataSource(_attributeActionProvider); var endpoints = endpointDataSource.Endpoints; + + AssertHasEndpoints(endpoints); } [Benchmark] @@ -66,6 +68,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance var endpointDataSource = CreateMvcEndpointDataSource(_conventionalActionProvider); endpointDataSource.ConventionalEndpointInfos.AddRange(_conventionalEndpointInfos); var endpoints = endpointDataSource.Endpoints; + + AssertHasEndpoints(endpoints); } private ActionDescriptor CreateAttributeRoutedAction(int id) @@ -110,11 +114,20 @@ namespace Microsoft.AspNetCore.Mvc.Performance var dataSource = new MvcEndpointDataSource( actionDescriptorCollectionProvider, new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), - new MockParameterPolicyFactory()); + new MockParameterPolicyFactory(), + new MockRoutePatternTransformer()); return dataSource; } + private class MockRoutePatternTransformer : RoutePatternTransformer + { + public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues) + { + return original; + } + } + private class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider { public MockActionDescriptorCollectionProvider(List actionDescriptors) @@ -137,5 +150,13 @@ namespace Microsoft.AspNetCore.Mvc.Performance throw new NotImplementedException(); } } + + private static void AssertHasEndpoints(IReadOnlyList endpoints) + { + if (endpoints.Count == 0) + { + throw new InvalidOperationException("Expected endpoints from data source."); + } + } } } diff --git a/src/Mvc/samples/MvcSandbox/SlugifyParameterTransformer.cs b/src/Mvc/samples/MvcSandbox/SlugifyParameterTransformer.cs new file mode 100644 index 0000000000..7c5b8c396f --- /dev/null +++ b/src/Mvc/samples/MvcSandbox/SlugifyParameterTransformer.cs @@ -0,0 +1,19 @@ +// 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.Text.RegularExpressions; +using Microsoft.AspNetCore.Routing; + +namespace MvcSandbox +{ + public class SlugifyParameterTransformer : IOutboundParameterTransformer + { + public string TransformOutbound(object value) + { + // Slugify value + return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2", RegexOptions.None, TimeSpan.FromMilliseconds(100)).ToLower(); + } + } +} + diff --git a/src/Mvc/samples/MvcSandbox/Startup.cs b/src/Mvc/samples/MvcSandbox/Startup.cs index dac9f9d057..58efaf78f0 100644 --- a/src/Mvc/samples/MvcSandbox/Startup.cs +++ b/src/Mvc/samples/MvcSandbox/Startup.cs @@ -5,11 +5,13 @@ using System; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -20,6 +22,10 @@ namespace MvcSandbox // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddRouting(options => + { + options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Latest); } @@ -37,6 +43,27 @@ namespace MvcSandbox name: "default", template: "{controller=Home}/{action=Index}/{id?}"); + builder.MapControllerRoute( + name: "transform", + template: "Transform/{controller:slugify=Home}/{action:slugify=Index}/{id?}", + defaults: null, + constraints: new { controller = "Home" }); + + builder.MapGet( + "/graph", + "DFA Graph", + (httpContext) => + { + using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true)) + { + var graphWriter = httpContext.RequestServices.GetRequiredService(); + var dataSource = httpContext.RequestServices.GetRequiredService(); + graphWriter.Write(dataSource, writer); + } + + return Task.CompletedTask; + }); + builder.MapApplication(); builder.MapHealthChecks("/healthz"); @@ -50,7 +77,7 @@ namespace MvcSandbox private static Task WriteEndpoints(HttpContext httpContext) { - var dataSource = httpContext.RequestServices.GetRequiredService(); + var dataSource = httpContext.RequestServices.GetRequiredService(); var sb = new StringBuilder(); sb.AppendLine("Endpoints:"); @@ -81,6 +108,7 @@ namespace MvcSandbox factory .AddConsole() .AddDebug(); + factory.SetMinimumLevel(LogLevel.Trace); }) .UseIISIntegration() .UseKestrel() diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs index 53e37a4595..3f1ef4d360 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs @@ -5,13 +5,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; 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.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; @@ -25,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing 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 @@ -37,26 +36,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing public MvcEndpointDataSource( IActionDescriptorCollectionProvider actions, MvcEndpointInvokerFactory invokerFactory, - ParameterPolicyFactory parameterPolicyFactory) + ParameterPolicyFactory parameterPolicyFactory, + RoutePatternTransformer routePatternTransformer) { - if (actions == null) - { - throw new ArgumentNullException(nameof(actions)); - } - - if (invokerFactory == null) - { - throw new ArgumentNullException(nameof(invokerFactory)); - } - - if (parameterPolicyFactory == null) - { - throw new ArgumentNullException(nameof(parameterPolicyFactory)); - } - _actions = actions; _invokerFactory = invokerFactory; _parameterPolicyFactory = parameterPolicyFactory; + _routePatternTransformer = routePatternTransformer; ConventionalEndpointInfos = new List(); AttributeRoutingConventionResolvers = new List>(); @@ -115,7 +101,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing lock (_lock) { var endpoints = new List(); - StringBuilder patternStringBuilder = null; foreach (var action in _actions.ActionDescriptors.Items) { @@ -129,48 +114,31 @@ namespace Microsoft.AspNetCore.Mvc.Routing // 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 the - // area/controller/action parameter parts replaced with literals - // - // e.g. {controller}/{action} with HomeController.Index and HomeController.Login - // would result in endpoints: - // - Home/Index - // - Home/Login + // 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 isApplicable = true; - foreach (var routeKey in action.RouteValues.Keys) - { - if (!MatchRouteValue(action, endpointInfo, routeKey)) - { - isApplicable = false; - break; - } - } + // 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 (!isApplicable) + if (updatedRoutePattern == null) { continue; } - conventionalRouteOrder = CreateEndpoints( - endpoints, - ref patternStringBuilder, + var endpoint = CreateEndpoint( action, - conventionalRouteOrder, - endpointInfo.ParsedPattern, - endpointInfo.MergedDefaults, - endpointInfo.Defaults, + updatedRoutePattern, endpointInfo.Name, + conventionalRouteOrder++, endpointInfo.DataTokens, - endpointInfo.ParameterPolicies, - suppressLinkGeneration: false, - suppressPathMatching: false, + false, + false, endpointInfo.Conventions); + endpoints.Add(endpoint); } } else @@ -185,20 +153,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template); - CreateEndpoints( - endpoints, - ref patternStringBuilder, + // 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, - action.AttributeRouteInfo.Order, - attributeRoutePattern, - attributeRoutePattern.Defaults, - nonInlineDefaults: null, + updatedRoutePattern, action.AttributeRouteInfo.Name, + action.AttributeRouteInfo.Order, dataTokens: null, - allParameterPolicies: null, action.AttributeRouteInfo.SuppressLinkGeneration, action.AttributeRouteInfo.SuppressPathMatching, conventionBuilder.Conventions); + endpoints.Add(endpoint); } } @@ -220,6 +195,58 @@ namespace Microsoft.AspNetCore.Mvc.Routing } } + 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) @@ -234,339 +261,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing return null; } - // CreateEndpoints processes the route pattern, replacing area/controller/action parameters with endpoint values - // Because of default values it is possible for a route pattern to resolve to multiple endpoints - private int CreateEndpoints( - List endpoints, - ref StringBuilder patternStringBuilder, - ActionDescriptor action, - int routeOrder, - RoutePattern routePattern, - IReadOnlyDictionary allDefaults, - IReadOnlyDictionary nonInlineDefaults, - string name, - RouteValueDictionary dataTokens, - IDictionary> allParameterPolicies, - bool suppressLinkGeneration, - bool suppressPathMatching, - List> conventions) - { - var newPathSegments = routePattern.PathSegments.ToList(); - var hasLinkGenerationEndpoint = false; - - // Create a mutable copy - var nonInlineDefaultsCopy = nonInlineDefaults != null - ? new RouteValueDictionary(nonInlineDefaults) - : null; - - var resolvedRouteValues = ResolveActionRouteValues(action, allDefaults); - - for (var i = 0; i < newPathSegments.Count; i++) - { - // Check if the pattern can be shortened because the remaining parameters are optional - // - // e.g. Matching pattern {controller=Home}/{action=Index} against HomeController.Index - // can resolve to the following endpoints: (sorted by RouteEndpoint.Order) - // - / - // - /Home - // - /Home/Index - if (UseDefaultValuePlusRemainingSegmentsOptional( - i, - action, - resolvedRouteValues, - allDefaults, - ref nonInlineDefaultsCopy, - newPathSegments)) - { - // The route pattern has matching default values AND an optional parameter - // For link generation we need to include an endpoint with parameters and default values - // so the link is correctly shortened - // e.g. {controller=Home}/{action=Index}/{id=17} - if (!hasLinkGenerationEndpoint) - { - var ep = CreateEndpoint( - action, - resolvedRouteValues, - name, - GetPattern(ref patternStringBuilder, newPathSegments), - newPathSegments, - nonInlineDefaultsCopy, - routeOrder++, - dataTokens, - suppressLinkGeneration, - true, - conventions); - endpoints.Add(ep); - - hasLinkGenerationEndpoint = true; - } - - var subPathSegments = newPathSegments.Take(i); - - var subEndpoint = CreateEndpoint( - action, - resolvedRouteValues, - name, - GetPattern(ref patternStringBuilder, subPathSegments), - subPathSegments, - nonInlineDefaultsCopy, - routeOrder++, - dataTokens, - suppressLinkGeneration, - suppressPathMatching, - conventions); - endpoints.Add(subEndpoint); - } - - UpdatePathSegments(i, action, resolvedRouteValues, routePattern, newPathSegments, ref allParameterPolicies); - } - - var finalEndpoint = CreateEndpoint( - action, - resolvedRouteValues, - name, - GetPattern(ref patternStringBuilder, newPathSegments), - newPathSegments, - nonInlineDefaultsCopy, - routeOrder++, - dataTokens, - suppressLinkGeneration, - suppressPathMatching, - conventions); - endpoints.Add(finalEndpoint); - - return routeOrder; - - string GetPattern(ref StringBuilder sb, IEnumerable segments) - { - if (sb == null) - { - sb = new StringBuilder(); - } - - RoutePatternWriter.WriteString(sb, segments); - var rawPattern = sb.ToString(); - sb.Length = 0; - - return rawPattern; - } - } - - private static IDictionary ResolveActionRouteValues(ActionDescriptor action, IReadOnlyDictionary allDefaults) - { - Dictionary resolvedRequiredValues = null; - - foreach (var kvp in action.RouteValues) - { - // Check whether there is a matching default value with a different case - // e.g. {controller=HOME}/{action} with HomeController.Index will have route values: - // - controller = HOME - // - action = Index - if (allDefaults.TryGetValue(kvp.Key, out var value) && - value is string defaultValue && - !string.Equals(kvp.Value, defaultValue, StringComparison.Ordinal) && - string.Equals(kvp.Value, defaultValue, StringComparison.OrdinalIgnoreCase)) - { - if (resolvedRequiredValues == null) - { - resolvedRequiredValues = new Dictionary(action.RouteValues, StringComparer.OrdinalIgnoreCase); - } - - resolvedRequiredValues[kvp.Key] = defaultValue; - } - } - - return resolvedRequiredValues ?? action.RouteValues; - } - - private void UpdatePathSegments( - int i, - ActionDescriptor action, - IDictionary resolvedRequiredValues, - RoutePattern routePattern, - List newPathSegments, - ref IDictionary> allParameterPolicies) - { - List segmentParts = null; // Initialize only as needed - var segment = newPathSegments[i]; - for (var j = 0; j < segment.Parts.Count; j++) - { - var part = segment.Parts[j]; - - if (part is RoutePatternParameterPart parameterPart) - { - if (resolvedRequiredValues.TryGetValue(parameterPart.Name, out var parameterRouteValue)) - { - if (segmentParts == null) - { - segmentParts = segment.Parts.ToList(); - } - if (allParameterPolicies == null) - { - allParameterPolicies = MvcEndpointInfo.BuildParameterPolicies(routePattern.Parameters, _parameterPolicyFactory); - } - - // Route value could be null if it is a "known" route value. - // Do not use the null value to de-normalize the route pattern, - // instead leave the parameter unchanged. - // e.g. - // RouteValues will contain a null "page" value if there are Razor pages - // Skip replacing the {page} parameter - if (parameterRouteValue != null) - { - if (allParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicies)) - { - // Check if the parameter has a transformer policy - // Use the first transformer policy - for (var k = 0; k < parameterPolicies.Count; k++) - { - if (parameterPolicies[k] is IOutboundParameterTransformer parameterTransformer) - { - parameterRouteValue = parameterTransformer.TransformOutbound(parameterRouteValue); - break; - } - } - } - - segmentParts[j] = RoutePatternFactory.LiteralPart(parameterRouteValue); - } - } - } - } - - // A parameter part was replaced so replace segment with updated parts - if (segmentParts != null) - { - newPathSegments[i] = RoutePatternFactory.Segment(segmentParts); - } - - } - - private bool UseDefaultValuePlusRemainingSegmentsOptional( - int segmentIndex, - ActionDescriptor action, - IDictionary resolvedRequiredValues, - IReadOnlyDictionary allDefaults, - ref RouteValueDictionary nonInlineDefaults, - List pathSegments) - { - // Check whether the remaining segments are all optional and one or more of them is - // for area/controller/action and has a default value - var usedDefaultValue = false; - - for (var i = segmentIndex; i < pathSegments.Count; i++) - { - var segment = pathSegments[i]; - for (var j = 0; j < segment.Parts.Count; j++) - { - var part = segment.Parts[j]; - if (part.IsParameter && part is RoutePatternParameterPart parameterPart) - { - if (allDefaults.TryGetValue(parameterPart.Name, out var v)) - { - if (resolvedRequiredValues.TryGetValue(parameterPart.Name, out var routeValue)) - { - if (string.Equals(v as string, routeValue, StringComparison.OrdinalIgnoreCase)) - { - usedDefaultValue = true; - continue; - } - } - else - { - if (nonInlineDefaults == null) - { - nonInlineDefaults = new RouteValueDictionary(); - } - nonInlineDefaults.TryAdd(parameterPart.Name, v); - - usedDefaultValue = true; - continue; - } - } - - if (parameterPart.IsOptional || parameterPart.IsCatchAll) - { - continue; - } - } - else if (part.IsSeparator && part is RoutePatternSeparatorPart separatorPart - && separatorPart.Content == ".") - { - // Check if this pattern ends in an optional extension, e.g. ".{ext?}" - // Current literal must be "." and followed by a single optional parameter part - var nextPartIndex = j + 1; - - if (nextPartIndex == segment.Parts.Count - 1 - && segment.Parts[nextPartIndex].IsParameter - && segment.Parts[nextPartIndex] is RoutePatternParameterPart extensionParameterPart - && extensionParameterPart.IsOptional) - { - continue; - } - } - - // Stop because there is a non-optional/non-defaulted trailing value - return false; - } - } - - return usedDefaultValue; - } - - private bool MatchRouteValue(ActionDescriptor action, MvcEndpointInfo endpointInfo, string routeKey) - { - if (!action.RouteValues.TryGetValue(routeKey, out var actionValue) || string.IsNullOrWhiteSpace(actionValue)) - { - // Action does not have a value for this routeKey, most likely because action is not in an area - // Check that the pattern does not have a parameter for the routeKey - var matchingParameter = endpointInfo.ParsedPattern.GetParameter(routeKey); - if (matchingParameter == null && - (!endpointInfo.ParsedPattern.Defaults.TryGetValue(routeKey, out var value) || - !string.IsNullOrEmpty(Convert.ToString(value)))) - { - return true; - } - } - else - { - if (endpointInfo.MergedDefaults != null && string.Equals(actionValue, endpointInfo.MergedDefaults[routeKey] as string, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - var matchingParameter = endpointInfo.ParsedPattern.GetParameter(routeKey); - if (matchingParameter != null) - { - // Check that the value matches against constraints on that parameter - // e.g. For {controller:regex((Home|Login))} the controller value must match the regex - if (endpointInfo.ParameterPolicies.TryGetValue(routeKey, out var parameterPolicies)) - { - foreach (var policy in parameterPolicies) - { - if (policy is IRouteConstraint constraint - && !constraint.Match(httpContext: null, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest)) - { - // Did not match constraint - return false; - } - } - } - - return true; - } - } - - return false; - } - private RouteEndpoint CreateEndpoint( ActionDescriptor action, - IDictionary actionRouteValues, + RoutePattern routePattern, string routeName, - string patternRawText, - IEnumerable segments, - object nonInlineDefaults, int order, RouteValueDictionary dataTokens, bool suppressLinkGeneration, @@ -583,16 +281,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing return invoker.InvokeAsync(); }; - var defaults = new RouteValueDictionary(nonInlineDefaults); - EnsureRequiredValuesInDefaults(actionRouteValues, defaults, segments); - - var model = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments), order); + var model = new RouteEndpointModel(requestDelegate, routePattern, order); AddEndpointMetadata( model.Metadata, action, routeName, - new RouteValueDictionary(actionRouteValues), dataTokens, suppressLinkGeneration, suppressPathMatching); @@ -616,7 +310,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing IList metadata, ActionDescriptor action, string routeName, - RouteValueDictionary requiredValues, RouteValueDictionary dataTokens, bool suppressLinkGeneration, bool suppressPathMatching) @@ -637,7 +330,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing metadata.Add(new DataTokensMetadata(dataTokens)); } - metadata.Add(new RouteValuesAddressMetadata(routeName, requiredValues)); + metadata.Add(new RouteNameMetadata(routeName)); // Add filter descriptors to endpoint metadata if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0) @@ -685,33 +378,5 @@ namespace Microsoft.AspNetCore.Mvc.Routing metadata.Add(new SuppressMatchingMetadata()); } } - - // Ensure route values are a subset of defaults - // Examples: - // - // Template: {controller}/{action}/{category}/{id?} - // Defaults(in-line or non in-line): category=products - // Required values: controller=foo, action=bar - // Final constructed pattern: foo/bar/{category}/{id?} - // Final defaults: controller=foo, action=bar, category=products - // - // Template: {controller=Home}/{action=Index}/{category=products}/{id?} - // Defaults: controller=Home, action=Index, category=products - // Required values: controller=foo, action=bar - // Final constructed pattern: foo/bar/{category}/{id?} - // Final defaults: controller=foo, action=bar, category=products - private void EnsureRequiredValuesInDefaults( - IDictionary routeValues, - RouteValueDictionary defaults, - IEnumerable segments) - { - foreach (var kvp in routeValues) - { - if (kvp.Value != null) - { - defaults[kvp.Key] = kvp.Value; - } - } - } } } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerLinkGeneratorExtensionsTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerLinkGeneratorExtensionsTest.cs index a07cad51cb..2629884624 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerLinkGeneratorExtensionsTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ControllerLinkGeneratorExtensionsTest.cs @@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var endpoint2 = CreateEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -55,11 +55,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var endpoint2 = CreateEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -83,11 +83,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var endpoint2 = CreateEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -114,11 +114,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var endpoint2 = CreateEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -144,11 +144,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var endpoint2 = CreateEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index" }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Routing { return new RouteEndpoint( (httpContext) => Task.CompletedTask, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), + RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues), order, new EndpointMetadataCollection(metadata ?? Array.Empty()), null); diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/EndpointRoutingUrlHelperTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/EndpointRoutingUrlHelperTest.cs index d6931df173..0f85a857fa 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/EndpointRoutingUrlHelperTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/EndpointRoutingUrlHelperTest.cs @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing "Home/Index", defaults: new { controller = "Home", action = "Index" }, requiredValues: new { controller = "Home", action = "Index" }, - metadataCollection: new EndpointMetadataCollection(new[] { new SuppressLinkGenerationMetadata() })); + metadata: new[] { new SuppressLinkGenerationMetadata() }); var urlHelper = CreateUrlHelper(new[] { endpoint }); // Act @@ -273,19 +273,20 @@ namespace Microsoft.AspNetCore.Mvc.Routing object requiredValues = null, int order = 0, string routeName = null, - EndpointMetadataCollection metadataCollection = null) + IList metadata = null) { - if (metadataCollection == null) + metadata = metadata ?? new List(); + + if (routeName != null) { - metadataCollection = new EndpointMetadataCollection( - new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues))); + metadata.Add(new RouteNameMetadata(routeName)); } return new RouteEndpoint( (httpContext) => Task.CompletedTask, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), + RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues), order, - metadataCollection, + new EndpointMetadataCollection(metadata), null); } 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 index 3fcbcc9d1b..d1c923b685 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/MvcEndpointDataSourceTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/MvcEndpointDataSourceTests.cs @@ -13,6 +13,7 @@ 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; @@ -64,14 +65,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); - Assert.NotNull(routeValuesAddressMetadata); - var endpointValue = routeValuesAddressMetadata.RequiredValues["Name"]; + 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); + Assert.Equal("/Template!", matcherEndpoint.RoutePattern.RawText); } [Fact] @@ -131,164 +130,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing Assert.True(actionInvokerCalled); } - public static TheoryData GetSingleActionData_Conventional - { - get => GetSingleActionData(true); - } - - public static TheoryData GetSingleActionData_Attribute - { - get => GetSingleActionData(false); - } - - private static TheoryData GetSingleActionData(bool isConventionalRouting) - { - var data = new TheoryData - { - {"{controller}/{action}/{id?}", null, new[] { "TestController/TestAction/{id?}" }}, - {"{controller}/{id?}", null, isConventionalRouting ? new string[] { } : new[] { "TestController/{id?}" }}, - {"{action}/{id?}", null, isConventionalRouting ? new string[] { } : new[] { "TestAction/{id?}" }}, - {"{Controller}/{Action}/{id?}", null, new[] { "TestController/TestAction/{id?}" }}, - {"{Controller}/{Action}/{id?}/{more?}", null, new[] { "TestController/TestAction/{id?}/{more?}" }}, - {"{CONTROLLER}/{ACTION}/{id?}", null, new[] { "TestController/TestAction/{id?}" }}, - {"{controller}/{action=TestAction}", "TestController/{action=TestAction}", new[] { "TestController", "TestController/TestAction" }}, - {"{controller}/{action=TestAction}/{id?}", "TestController/{action=TestAction}/{id?}", new[] { "TestController", "TestController/TestAction/{id?}" }}, - {"{controller}/{action=TESTACTION}/{id?}", "TestController/{action=TESTACTION}/{id?}", new[] { "TestController", "TestController/TESTACTION/{id?}" }}, - {"{controller}/{action=TestAction}/{id?}/{more}", null, new[] { "TestController/TestAction/{id?}/{more}" }}, - {"{controller=TestController}/{action=TestAction}/{id?}", "{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestController", "TestController/TestAction/{id?}" }}, - {"{controller=TestController}/{action=TestAction}/{id?}/{more?}", "{controller=TestController}/{action=TestAction}/{id?}/{more?}", new[] { "", "TestController", "TestController/TestAction/{id?}/{more?}" }}, - {"{controller}/{action}/{*catchAll}", null, new[] { "TestController/TestAction/{*catchAll}" }}, - {"{controller}/{action=TestAction}/{*catchAll}", "TestController/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" }}, - {"{controller}/{action=TestAction}/{id?}/{*catchAll}", "TestController/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" }}, - {"{controller}/{action=TestAction}/{id?}/{**catchAll}", "TestController/{action=TestAction}/{id?}/{**catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{**catchAll}" }}, - {"{controller}/{action}.{ext?}", null, new[] { "TestController/TestAction.{ext?}" }}, - {"{controller}/{action=TestAction}.{ext?}", "TestController/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" }}, - {"{controller}/{action=TestAction}.{ext?}/{more?}", "TestController/{action=TestAction}.{ext?}/{more?}", new[] { "TestController", "TestController/TestAction.{ext?}/{more?}" }}, - {"{controller}/{action=TestAction}.{ext?}/{more}", null, new[] { "TestController/TestAction.{ext?}/{more}" }}, - {"{controller:upper-case}/{action:upper-case=TestAction}.{ext?}", "TESTCONTROLLER/{action:upper-case=TestAction}.{ext?}", new[] { "TESTCONTROLLER", "TESTCONTROLLER/TESTACTION.{ext?}" }}, - }; - - return data; - } - - [Theory] - [MemberData(nameof(GetSingleActionData_Conventional))] - public void Endpoints_Conventional_SingleAction(string endpointInfoRoute, string suppressMatchingTemplate, string[] finalEndpointPatterns) - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); - - // Act - var endpoints = dataSource.Endpoints.ToList(); - - // Assert - - // Ensure there are no endpoints with duplicate Order values - Assert.DoesNotContain(endpoints.GroupBy(e => Assert.IsType(e).Order), g => g.Count() > 1); - - endpoints = endpoints.OrderBy(e => Assert.IsType(e).Order).ToList(); - - AssertSuppressMatchingTemplate(suppressMatchingTemplate, endpoints); - - var inspectors = finalEndpointPatterns - .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) - .ToArray(); - - // Assert - Assert.Collection(endpoints, inspectors); - } - - [Theory] - [MemberData(nameof(GetSingleActionData_Attribute))] - public void Endpoints_AttributeRouting_SingleAction(string endpointInfoRoute, string suppressMatchingTemplate, string[] finalEndpointPatterns) - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - attributeRouteTemplate: endpointInfoRoute, - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - - // Act - var endpoints = dataSource.Endpoints.ToList(); - - // Ensure there are no endpoints with duplicate Order values - Assert.DoesNotContain(endpoints.GroupBy(e => Assert.IsType(e).Order), g => g.Count() > 1); - - endpoints = endpoints.OrderBy(e => Assert.IsType(e).Order).ToList(); - - AssertSuppressMatchingTemplate(suppressMatchingTemplate, endpoints); - - // Assert - var inspectors = finalEndpointPatterns - .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) - .ToArray(); - - // Assert - Assert.Collection(endpoints, inspectors); - } - - [Theory] - [InlineData("{area}/{controller}/{action}/{id?}", null, new[] { "TestArea/TestController/TestAction/{id?}" })] - [InlineData("{controller}/{action}/{id?}", null, new string[] { })] - [InlineData("{area=TestArea}/{controller}/{action}/{id?}", null, new[] { "TestArea/TestController/TestAction/{id?}" })] - [InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", "TestArea/TestController/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}"})] - [InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", "{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })] - [InlineData("{area:exists}/{controller}/{action}/{id?}", null, new[] { "TestArea/TestController/TestAction/{id?}" })] - [InlineData("{area:exists:upper-case}/{controller}/{action}/{id?}", null, new[] { "TESTAREA/TestController/TestAction/{id?}" })] - public void Endpoints_AreaSingleAction(string endpointInfoRoute, string suppressMatchingTemplate, string[] finalEndpointTemplates) - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction", area = "TestArea" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - - var services = new ServiceCollection(); - services.AddRouting(); - services.AddSingleton(actionDescriptorCollection); - - var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); - services.Configure(routeOptionsSetup.Configure); - services.Configure(options => - { - options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); - }); - - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute, serviceProvider: services.BuildServiceProvider())); - - // Act - var endpoints = dataSource.Endpoints.ToList(); - - // Assert - - // Ensure there are no endpoints with duplicate Order values - Assert.DoesNotContain(endpoints.GroupBy(e => Assert.IsType(e).Order), g => g.Count() > 1); - - endpoints = endpoints.OrderBy(e => Assert.IsType(e).Order).ToList(); - - AssertSuppressMatchingTemplate(suppressMatchingTemplate, endpoints); - - var inspectors = finalEndpointTemplates - .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) - .ToArray(); - - // Assert - Assert.Collection(endpoints, inspectors); - } - - private static void AssertSuppressMatchingTemplate(string suppressMatchingTemplate, List endpoints) - { - if (suppressMatchingTemplate != null) - { - var suppressMatchingEndpoint = endpoints.First(); - Assert.True(suppressMatchingEndpoint.Metadata.GetMetadata()?.SuppressMatching); - Assert.Equal(suppressMatchingTemplate, Assert.IsType(suppressMatchingEndpoint).RoutePattern.RawText); - endpoints.Remove(suppressMatchingEndpoint); - } - } - [Fact] public void Endpoints_SingleAction_ConventionalRoute_ContainsParameterWithNullRequiredRouteValue() { @@ -321,34 +162,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing var endpoints = dataSource.Endpoints; // Assert - Assert.Collection(endpoints, - (e) => Assert.Equal("TestController/TestAction/{page}", Assert.IsType(e).RoutePattern.RawText)); - } - - [Fact] - public void Endpoints_SingleAction_WithActionDefault() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - string.Empty, - "{controller}/{action}", - new RouteValueDictionary(new { action = "TestAction" }))); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection(endpoints, + Assert.Collection(endpoints.Cast(), (e) => { - Assert.Equal("TestController/{action=TestAction}", Assert.IsType(e).RoutePattern.RawText); - Assert.True(e.Metadata.GetMetadata().SuppressMatching); - }, - (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), - (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); + 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] @@ -375,9 +196,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing // Assert Assert.Collection(endpoints1, - (e) => Assert.Equal("TestController/{action=TestAction}", Assert.IsType(e).RoutePattern.RawText), - (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), - (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); + (e) => Assert.Equal("{controller}/{action}", Assert.IsType(e).RoutePattern.RawText)); Assert.Same(endpoints1, endpoints2); actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once); @@ -417,9 +236,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing var endpoints = dataSource.Endpoints; Assert.Collection(endpoints, - (e) => Assert.Equal("TestController/{action=TestAction}", Assert.IsType(e).RoutePattern.RawText), - (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), - (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); + (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) @@ -435,7 +258,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing Assert.NotSame(endpoints, newEndpoints); Assert.Collection(newEndpoints, - (e) => Assert.Equal("NewTestController/NewTestAction", Assert.IsType(e).RoutePattern.RawText)); + (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] @@ -456,37 +285,19 @@ namespace Microsoft.AspNetCore.Mvc.Routing var endpoints = dataSource.Endpoints; // Assert - Assert.Collection(endpoints, - (e) => Assert.Equal("TestController/TestAction1", Assert.IsType(e).RoutePattern.RawText), - (e) => Assert.Equal("TestController/TestAction2", Assert.IsType(e).RoutePattern.RawText)); - } - - [Theory] - [InlineData("{controller}/{action}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController1/TestAction3", "TestController2/TestAction1" })] - [InlineData("{controller}/{action:regex((TestAction1|TestAction2))}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController2/TestAction1" })] - [InlineData("{controller}/{action:regex((TestAction1|TestAction2)):upper-case}", new[] { "TestController1/TESTACTION1", "TestController1/TESTACTION2", "TestController2/TESTACTION1" })] - public void Endpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates) - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController1", action = "TestAction1" }, - new { controller = "TestController1", action = "TestAction2" }, - new { controller = "TestController1", action = "TestAction3" }, - new { controller = "TestController2", action = "TestAction1" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - string.Empty, - endpointInfoRoute)); - - // Act - var endpoints = dataSource.Endpoints; - - var inspectors = finalEndpointTemplates - .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) - .ToArray(); - - // Assert - Assert.Collection(endpoints, inspectors); + 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] @@ -505,9 +316,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - var routeValuesAddressNameMetadata = matcherEndpoint.Metadata.GetMetadata(); - Assert.NotNull(routeValuesAddressNameMetadata); - Assert.Equal(string.Empty, routeValuesAddressNameMetadata.RouteName); + var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); + Assert.NotNull(routeNameMetadata); + Assert.Equal(string.Empty, routeNameMetadata.RouteName); } [Fact] @@ -530,18 +341,18 @@ namespace Microsoft.AspNetCore.Mvc.Routing (ep) => { var matcherEndpoint = Assert.IsType(ep); - var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); - Assert.NotNull(routeValuesAddressMetadata); - Assert.Equal("namedRoute", routeValuesAddressMetadata.RouteName); - Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); + 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 routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); - Assert.NotNull(routeValuesAddressMetadata); - Assert.Equal("namedRoute", routeValuesAddressMetadata.RouteName); - Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); + var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); + Assert.NotNull(routeNameMetadata); + Assert.Equal("namedRoute", routeNameMetadata.RouteName); + Assert.Equal("named/{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText); }); } @@ -569,25 +380,33 @@ namespace Microsoft.AspNetCore.Mvc.Routing (ep) => { var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); + 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/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); + 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("Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); + 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/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); + 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); }); } @@ -631,10 +450,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing // Assert Assert.Collection( - endpoints.Cast().OrderBy(e => e.RoutePattern.RawText), - e => Assert.Equal("en-CA/home/about", e.RoutePattern.RawText), - e => Assert.Equal("en-NZ/home/index", e.RoutePattern.RawText), - e => Assert.Equal("home/index", e.RoutePattern.RawText)); + 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] @@ -689,8 +525,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing public void NoDefaultValues_RequiredValues_UsedToCreateDefaultValues() { // Arrange - var expectedDefaults = new RouteValueDictionary(new { controller = "Foo", action = "Bar" }); - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: expectedDefaults); + 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}")); @@ -700,30 +536,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText); - AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); - } - - [Fact] - public void RequiredValues_NotPresent_InDefaultValues_IsAddedToDefaultValues() - { - // 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, "{subarea}/{controller=Home}/{action=Index}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - var endpoint = Assert.Single(endpoints); - var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("test/Foo/Bar", matcherEndpoint.RoutePattern.RawText); - AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); + Assert.Equal("{controller}/{action}", matcherEndpoint.RoutePattern.RawText); + AssertIsSubset(requiredValues, matcherEndpoint.RoutePattern.RequiredValues); } [Fact] @@ -745,51 +559,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing Assert.Empty(endpoints); } - [Fact] - public void RequiredValues_IsSubsetOf_DefaultValues() - { - // Arrange - var requiredValues = new RouteValueDictionary( - new { controller = "Foo", action = "Bar", subarea = "test" }); - var expectedDefaults = new RouteValueDictionary( - new { controller = "Foo", action = "Bar", subarea = "test", subscription = "general" }); - var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add( - CreateEndpointInfo( - string.Empty, - "{controller=Home}/{action=Index}/{subscription=general}", - defaults: new RouteValueDictionary(new { subarea = "test", }))); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(3, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - [Fact] public void RequiredValues_DoesNotMatchParameterDefaults_Included() { @@ -812,8 +581,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("Foo/Baz/{id?}", matcherEndpoint.RoutePattern.RawText); - AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); + 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] @@ -844,147 +616,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } - [Fact] - public void Endpoints_ConventionalRoutes_NonDefaultAndDefaultValuesEndingWithOptional_IncludeFullRouteAsHighPriority() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "Home", action = "Index" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller}/{action=Index}/{id?}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Home/{action=Index}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Home", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(3, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - - [Fact] - public void Endpoints_ConventionalRoutes_DefaultValuesEndingWithOptional_IncludeFullRouteAsHighPriority() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "Home", action = "Index" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller=Home}/{action=Index}/{id?}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller=Home}/{action=Index}/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Home", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(3, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(4, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - - [Fact] - public void Endpoints_ConventionalRoutes_DefaultValues_Shortened() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller=TestController}/{action=TestAction}/{id=17}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller=TestController}/{action=TestAction}/{id=17}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(1, matcherEndpoint.Order); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(2, matcherEndpoint.Order); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(3, matcherEndpoint.Order); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(4, matcherEndpoint.Order); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction/{id=17}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(5, matcherEndpoint.Order); - }); - } - [Fact] public void Endpoints_ConventionalRoutes_DefaultValuesAndCatchAll_EndpointInfoDefaultsNotModified() { @@ -1006,251 +637,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing Assert.Empty(endpointInfo.Defaults); } - [Fact] - public void Endpoints_ConventionalRoutes_DefaultValuesAndCatchAll_Shortened() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(3, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(4, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction/{id=17}/{**catchAll}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(5, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - - [Fact] - public void Endpoints_ConventionalRoutes_DefaultValuesAndOptional_Shortened() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller=TestController}/{action=TestAction}/{id=17}/{more?}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller=TestController}/{action=TestAction}/{id=17}/{more?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(3, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(4, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction/{id=17}/{more?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("17", matcherEndpoint.RoutePattern.Defaults["id"]); - Assert.Equal(5, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - - [Fact] - public void Endpoints_ConventionalRoutes_OptionalExtension_IncludeFullRouteAsHighPriority() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller}/{action=TestAction}.{ext?}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/{action=TestAction}.{ext?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction.{ext?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(3, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - - [Fact] - public void Endpoints_ConventionalRoutes_MultipleOptionalAndCatchAll_IncludeFullRouteAsHighPriority() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - new { controller = "TestController", action = "TestAction" }); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( - name: string.Empty, - template: "{controller=TestController}/{action=TestAction}/{id?}/{more?}/{**catchAll}")); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("{controller=TestController}/{action=TestAction}/{id?}/{more?}/{**catchAll}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(3, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction/{id?}/{more?}/{**catchAll}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(4, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - - [Fact] - public void Endpoints_AttributeRoutes_CatchAllWithDefault_IncludeFullRouteAsHighPriority() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - "/TeamName/{*Name=DefaultName}/", - 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("TeamName/{*Name=DefaultName}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(0, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TeamName", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("DefaultName", matcherEndpoint.RoutePattern.Defaults["Name"]); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TeamName/{*Name=DefaultName}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("DefaultName", matcherEndpoint.RoutePattern.Defaults["Name"]); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - }); - } - [Fact] public void Endpoints_AttributeRoutes_DefaultDifferentCaseFromRouteValue_UseDefaultCase() { @@ -1269,66 +655,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing (ep) => { var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/{action=TESTACTION}/{id?}", matcherEndpoint.RoutePattern.RawText); + Assert.Equal("{controller}/{action=TESTACTION}/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal("TESTACTION", matcherEndpoint.RoutePattern.Defaults["action"]); Assert.Equal(0, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, true); - var routeValuesAddress = matcherEndpoint.Metadata.GetMetadata(); - Assert.Equal("TESTACTION", routeValuesAddress.RequiredValues["action"]); - - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("TESTACTION", matcherEndpoint.RoutePattern.Defaults["action"]); - Assert.Equal(1, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - - var routeValuesAddress = matcherEndpoint.Metadata.GetMetadata(); - Assert.Equal("TESTACTION", routeValuesAddress.RequiredValues["action"]); - }, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TESTACTION/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal("TESTACTION", matcherEndpoint.RoutePattern.Defaults["action"]); - Assert.Equal(2, matcherEndpoint.Order); - AssertMatchingSuppressed(matcherEndpoint, false); - - var routeValuesAddress = matcherEndpoint.Metadata.GetMetadata(); - Assert.Equal("TESTACTION", routeValuesAddress.RequiredValues["action"]); - }); - } - - [Fact] - public void Endpoints_AttributeRoutes_ActionMetadataDoesNotOverrideDataSourceMetadata() - { - // Arrange - var actionDescriptorCollection = GetActionDescriptorCollection( - CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }, - "{controller}/{action}/{id?}", - new List { new RouteValuesAddressMetadata("fakeroutename", new RouteValueDictionary(new { fake = "Fake!" })) }) - ); - var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); - - // Act - var endpoints = dataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => - { - var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("TestController/TestAction/{id?}", matcherEndpoint.RoutePattern.RawText); - Assert.Equal(0, matcherEndpoint.Order); - - var routeValuesAddress = matcherEndpoint.Metadata.GetMetadata(); - Assert.Equal("{controller}/{action}/{id?}", routeValuesAddress.RouteName); - Assert.Equal("TestController", routeValuesAddress.RequiredValues["controller"]); - Assert.Equal("TestAction", routeValuesAddress.RequiredValues["action"]); + Assert.Equal("TestAction", matcherEndpoint.RoutePattern.RequiredValues["action"]); }); } @@ -1345,16 +676,21 @@ namespace Microsoft.AspNetCore.Mvc.Routing 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(), + serviceProvider.GetRequiredService()); var defaultEndpointConventionBuilder = new DefaultEndpointConventionBuilder(); dataSource.AttributeRoutingConventionResolvers.Add((actionDescriptor) => @@ -1387,14 +723,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing 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); - }); + var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); + services.Configure(routeOptionsSetup.Configure); + services.Configure(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); - serviceProvider = services.BuildServiceProvider(); + serviceProvider = services.BuildServiceProvider(); } var parameterPolicyFactory = serviceProvider.GetRequiredService(); diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs index c0b84528e0..0117674a66 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs @@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "About/{id}", defaults: new { page = "/About", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) }); + requiredValues:new { page = "/About", }); var endpoint2 = CreateEndpoint( "Admin/ManageUsers/{handler?}", defaults: new { page = "/Admin/ManageUsers", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) }); + requiredValues:new { page = "/Admin/ManageUsers", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -54,11 +54,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "About/{id}", defaults: new { page = "/About", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) }); + requiredValues:new { page = "/About", }); var endpoint2 = CreateEndpoint( "Admin/ManageUsers/{handler?}", defaults: new { page = "/Admin/ManageUsers", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) }); + requiredValues:new { page = "/Admin/ManageUsers", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -82,11 +82,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "About/{id}", defaults: new { page = "/About", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) }); + requiredValues:new { page = "/About", }); var endpoint2 = CreateEndpoint( "Admin/ManageUsers", defaults: new { page = "/Admin/ManageUsers", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) }); + requiredValues:new { page = "/Admin/ManageUsers", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -112,11 +112,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "About/{id}", defaults: new { page = "/About", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) }); + requiredValues:new { page = "/About", }); var endpoint2 = CreateEndpoint( "Admin/ManageUsers", defaults: new { page = "/Admin/ManageUsers", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) }); + requiredValues:new { page = "/Admin/ManageUsers", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -142,11 +142,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "About/{id}", defaults: new { page = "/About", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/About", })) }); + requiredValues:new { page = "/About", }); var endpoint2 = CreateEndpoint( "Admin/ManageUsers", defaults: new { page = "/Admin/ManageUsers", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { page = "/Admin/ManageUsers", })) }); + requiredValues:new { page = "/Admin/ManageUsers", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Routing { return new RouteEndpoint( (httpContext) => Task.CompletedTask, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), + RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues), order, new EndpointMetadataCollection(metadata ?? Array.Empty()), null); diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 4a11189cb2..1b833e2ce8 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -323,7 +323,7 @@ Hello from page"; var expected = @"Link inside area Link to external area -Link to area action +Link to area action Link to non-area page"; // Act diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs index c2a0455553..50a237234d 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs @@ -1294,8 +1294,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("/", result.Link); } - - [Fact] public virtual async Task AttributeRoutedAction_InArea_LinkToConventionalRoutedActionInArea() { diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.About.html b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.About.html index 72ba50bb99..d30a0a1a73 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.About.html +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.About.html @@ -1,4 +1,4 @@ - + @@ -12,8 +12,8 @@

ASP.NET vNext - About

| My Home - | My About - | My Help |

+ | My About + | My Help |

diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Help.html b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Help.html index a3d85388ba..6d1ecf32b7 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Help.html +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Help.html @@ -1,4 +1,4 @@ - + @@ -12,8 +12,8 @@

ASP.NET vNext - Help

| My Home - | My About - | My Help |

+ | My About + | My Help |

diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Index.html b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Index.html index d10ba0b6f4..9e85fde939 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Index.html +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.Index.html @@ -1,4 +1,4 @@ - + @@ -19,8 +19,8 @@

ASP.NET vNext - Home Page

| My Home - | My About - | My Help |

+ | My About + | My Help |

diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.ViewComponentTagHelpers.html b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.ViewComponentTagHelpers.html index c9bec6c144..3fbc0e8395 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.ViewComponentTagHelpers.html +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Home.ViewComponentTagHelpers.html @@ -1,4 +1,4 @@ - + @@ -12,8 +12,8 @@

ASP.NET vNext -

| My Home - | My About - | My Help |

+ | My About + | My Help |

Items:
diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Controllers/DebugController.cs b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/DebugController.cs new file mode 100644 index 0000000000..b217902a6c --- /dev/null +++ b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/DebugController.cs @@ -0,0 +1,31 @@ +// 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.IO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Internal; + +namespace RoutingWebSite +{ + // This controller is reachable via traditional routing. + public class DebugController : Controller + { + private readonly DfaGraphWriter _graphWriter; + private readonly EndpointDataSource _endpointDataSource; + + public DebugController(DfaGraphWriter graphWriter, EndpointDataSource endpointDataSource) + { + _graphWriter = graphWriter; + _endpointDataSource = endpointDataSource; + } + + public IActionResult Graph() + { + var sw = new StringWriter(); + _graphWriter.Write(_endpointDataSource, sw); + + return Content(sw.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs b/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs index 44bc08a25f..1f1be144e4 100644 --- a/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/RoutingWebSite/Startup.cs @@ -1,12 +1,14 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection; namespace RoutingWebSite