diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/EndpointRoutingBenchmarkBase.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/EndpointRoutingBenchmarkBase.cs index 58c50f1225..6a61ff6693 100644 --- a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/EndpointRoutingBenchmarkBase.cs +++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/EndpointRoutingBenchmarkBase.cs @@ -116,11 +116,14 @@ namespace Microsoft.AspNetCore.Routing params object[] metadata) { var endpointMetadata = new List(metadata ?? Array.Empty()); - endpointMetadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues))); + if (routeName != null) + { + endpointMetadata.Add(new RouteNameMetadata(routeName)); + } return new RouteEndpoint( (context) => Task.CompletedTask, - RoutePatternFactory.Parse(template, defaults, constraints), + RoutePatternFactory.Parse(template, defaults, constraints, requiredValues), order, new EndpointMetadataCollection(endpointMetadata), displayName); @@ -139,16 +142,13 @@ namespace Microsoft.AspNetCore.Routing protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint) { - var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata(); - var requiredValues = routeValuesAddressMetadata?.RequiredValues ?? new RouteValueDictionary(); - treeRouteBuilder.MapOutbound( NullRouter.Instance, new RouteTemplate(RoutePatternFactory.Parse( endpoint.RoutePattern.RawText, defaults: endpoint.RoutePattern.Defaults, parameterPolicies: null)), - requiredLinkValues: new RouteValueDictionary(requiredValues), + requiredLinkValues: new RouteValueDictionary(endpoint.RoutePattern.RequiredValues), routeName: null, order: 0); } diff --git a/src/Routing/samples/RoutingSandbox/Framework/FrameworkConfigurationBuilder.cs b/src/Routing/samples/RoutingSandbox/Framework/FrameworkConfigurationBuilder.cs new file mode 100644 index 0000000000..5bcd785651 --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/Framework/FrameworkConfigurationBuilder.cs @@ -0,0 +1,38 @@ +// 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.Http; +using Microsoft.AspNetCore.Routing.Patterns; + +namespace RoutingSandbox.Framework +{ + public class FrameworkConfigurationBuilder + { + private readonly FrameworkEndpointDataSource _dataSource; + + internal FrameworkConfigurationBuilder(FrameworkEndpointDataSource dataSource) + { + _dataSource = dataSource; + } + + public void AddPattern(string pattern) + { + AddPattern(RoutePatternFactory.Parse(pattern)); + } + + public void AddPattern(RoutePattern pattern) + { + _dataSource.Patterns.Add(pattern); + } + + public void AddHubMethod(string hub, string method, RequestDelegate requestDelegate) + { + _dataSource.HubMethods.Add(new HubMethod + { + Hub = hub, + Method = method, + RequestDelegate = requestDelegate + }); + } + } +} diff --git a/src/Routing/samples/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs b/src/Routing/samples/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs new file mode 100644 index 0000000000..02b4d55c67 --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +namespace RoutingSandbox.Framework +{ + internal class FrameworkEndpointDataSource : EndpointDataSource, IEndpointConventionBuilder + { + private readonly RoutePatternTransformer _routePatternTransformer; + private readonly List> _conventions; + + public List Patterns { get; } + public List HubMethods { get; } + + private List _endpoints; + + public FrameworkEndpointDataSource(RoutePatternTransformer routePatternTransformer) + { + _routePatternTransformer = routePatternTransformer; + _conventions = new List>(); + + Patterns = new List(); + HubMethods = new List(); + } + + public override IReadOnlyList Endpoints + { + get + { + if (_endpoints == null) + { + _endpoints = BuildEndpoints(); + } + + return _endpoints; + } + } + + private List BuildEndpoints() + { + List endpoints = new List(); + + foreach (var hubMethod in HubMethods) + { + var requiredValues = new { hub = hubMethod.Hub, method = hubMethod.Method }; + var order = 1; + + foreach (var pattern in Patterns) + { + var resolvedPattern = _routePatternTransformer.SubstituteRequiredValues(pattern, requiredValues); + if (resolvedPattern == null) + { + continue; + } + + var endpointModel = new RouteEndpointModel( + hubMethod.RequestDelegate, + resolvedPattern, + order++); + endpointModel.DisplayName = $"{hubMethod.Hub}.{hubMethod.Method}"; + + foreach (var convention in _conventions) + { + convention(endpointModel); + } + + endpoints.Add(endpointModel.Build()); + } + } + + return endpoints; + } + + public override IChangeToken GetChangeToken() + { + return NullChangeToken.Singleton; + } + + public void Apply(Action convention) + { + _conventions.Add(convention); + } + } + + internal class HubMethod + { + public string Hub { get; set; } + public string Method { get; set; } + public RequestDelegate RequestDelegate { get; set; } + } +} diff --git a/src/Routing/samples/RoutingSandbox/Framework/FrameworkEndpointRouteBuilderExtensions.cs b/src/Routing/samples/RoutingSandbox/Framework/FrameworkEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..6371160ef8 --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/Framework/FrameworkEndpointRouteBuilderExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; + +namespace RoutingSandbox.Framework +{ + public static class FrameworkEndpointRouteBuilderExtensions + { + public static IEndpointConventionBuilder MapFramework(this IEndpointRouteBuilder builder, Action configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var dataSource = builder.ServiceProvider.GetRequiredService(); + + var configurationBuilder = new FrameworkConfigurationBuilder(dataSource); + configure(configurationBuilder); + + builder.DataSources.Add(dataSource); + + return dataSource; + } + } +} diff --git a/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj b/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj index eaf327a420..bf55c4e2a0 100644 --- a/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj +++ b/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 diff --git a/src/Routing/samples/RoutingSandbox/SlugifyParameterTransformer.cs b/src/Routing/samples/RoutingSandbox/SlugifyParameterTransformer.cs new file mode 100644 index 0000000000..4838fcc89e --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/SlugifyParameterTransformer.cs @@ -0,0 +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 System; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Routing; + +namespace RoutingSandbox +{ + 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/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs b/src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs index fdeabd02b9..88c48cf150 100644 --- a/src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs +++ b/src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs @@ -10,9 +10,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; -using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; +using RoutingSandbox.Framework; namespace RoutingSandbox { @@ -23,7 +22,11 @@ namespace RoutingSandbox public void ConfigureServices(IServiceCollection services) { - services.AddRouting(); + services.AddRouting(options => + { + options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); + services.AddSingleton(); } public void Configure(IApplicationBuilder app) @@ -72,12 +75,22 @@ namespace RoutingSandbox using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true)) { var graphWriter = httpContext.RequestServices.GetRequiredService(); - var dataSource = httpContext.RequestServices.GetRequiredService(); + var dataSource = httpContext.RequestServices.GetRequiredService(); graphWriter.Write(dataSource, writer); } return Task.CompletedTask; }); + + builder.MapFramework(frameworkBuilder => + { + frameworkBuilder.AddPattern("/transform/{hub:slugify=TestHub}/{method:slugify=TestMethod}"); + frameworkBuilder.AddPattern("/{hub}/{method=TestMethod}"); + + frameworkBuilder.AddHubMethod("TestHub", "TestMethod", context => context.Response.WriteAsync("TestMethod!")); + frameworkBuilder.AddHubMethod("Login", "Authenticate", context => context.Response.WriteAsync("Authenticate!")); + frameworkBuilder.AddHubMethod("Login", "Logout", context => context.Response.WriteAsync("Logout!")); + }); }); app.UseStaticFiles(); diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs index d9bc0f559a..e6b1c0a19a 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs @@ -167,13 +167,14 @@ namespace Microsoft.AspNetCore.Routing sb.Append(", Defaults: new { "); sb.Append(string.Join(", ", FormatValues(routeEndpoint.RoutePattern.Defaults))); sb.Append(" }"); - var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata(); + var routeNameMetadata = routeEndpoint.Metadata.GetMetadata(); sb.Append(", Route Name: "); - sb.Append(routeValuesAddressMetadata?.RouteName); - if (routeValuesAddressMetadata?.RequiredValues != null) + sb.Append(routeNameMetadata?.RouteName); + var routeValues = routeEndpoint.RoutePattern.RequiredValues; + if (routeValues.Count > 0) { sb.Append(", Required Values: new { "); - sb.Append(string.Join(", ", FormatValues(routeValuesAddressMetadata.RequiredValues))); + sb.Append(string.Join(", ", FormatValues(routeValues))); sb.Append(" }"); } sb.Append(", Order: "); diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index 2e04322698..60342bfb1c 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -311,7 +311,7 @@ namespace Microsoft.AspNetCore.Routing _uriBuildingContextPool, endpoint.RoutePattern, new RouteValueDictionary(endpoint.RoutePattern.Defaults), - endpoint.Metadata.GetMetadata()?.RequiredValues.Keys, + endpoint.RoutePattern.RequiredValues.Keys, policies); } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteNameMetadata.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteNameMetadata.cs new file mode 100644 index 0000000000..6fbffb88ef --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteNameMetadata.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Represents metadata used during link generation to find + /// the associated endpoint using route name. + /// + public interface IRouteNameMetadata + { + /// + /// Gets the route name. Can be null. + /// + string RouteName { get; } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs index 076dea8a92..1bdae8727b 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs @@ -1,6 +1,7 @@ // 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; namespace Microsoft.AspNetCore.Routing @@ -9,6 +10,7 @@ namespace Microsoft.AspNetCore.Routing /// Represents metadata used during link generation to find /// the associated endpoint using route values. /// + [Obsolete("Route values are now specified on a RoutePattern.")] public interface IRouteValuesAddressMetadata { /// diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs index 1b2172db53..b5b3721b23 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs @@ -143,24 +143,12 @@ namespace Microsoft.AspNetCore.Routing.Matching { var parent = parents[j]; var part = segment.Parts[0]; + var parameterPart = part as RoutePatternParameterPart; if (segment.IsSimple && part is RoutePatternLiteralPart literalPart) { - DfaNode next = null; - var literal = literalPart.Content; - if (parent.Literals == null || - !parent.Literals.TryGetValue(literal, out next)) - { - next = new DfaNode() - { - PathDepth = parent.PathDepth + 1, - Label = includeLabel ? parent.Label + literal + "/" : null, - }; - parent.AddLiteral(literal, next); - } - - nextParents.Add(next); + AddLiteralNode(includeLabel, nextParents, parent, literalPart.Content); } - else if (segment.IsSimple && part is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll) + else if (segment.IsSimple && parameterPart != null && parameterPart.IsCatchAll) { // A catch all should traverse all literal nodes as well as parameter nodes // we don't need to create the parameter node here because of ordering @@ -194,7 +182,29 @@ namespace Microsoft.AspNetCore.Routing.Matching parent.CatchAll.AddMatch(endpoint); } - else if (segment.IsSimple && part.IsParameter) + else if (segment.IsSimple && parameterPart != null && TryGetRequiredValue(endpoint.RoutePattern, parameterPart, out var requiredValue)) + { + // If the parameter has a matching required value, replace the parameter with the required value + // as a literal. This should use the parameter's transformer (if present) + // e.g. Template: Home/{action}, Required values: { action = "Index" }, Result: Home/Index + + if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicyReferences)) + { + for (var k = 0; k < parameterPolicyReferences.Count; k++) + { + var reference = parameterPolicyReferences[k]; + var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference); + if (parameterPolicy is IOutboundParameterTransformer parameterTransformer) + { + requiredValue = parameterTransformer.TransformOutbound(requiredValue); + break; + } + } + } + + AddLiteralNode(includeLabel, nextParents, parent, requiredValue.ToString()); + } + else if (segment.IsSimple && parameterPart != null) { if (parent.Parameters == null) { @@ -254,6 +264,23 @@ namespace Microsoft.AspNetCore.Routing.Matching return root; } + private static void AddLiteralNode(bool includeLabel, List nextParents, DfaNode parent, string literal) + { + DfaNode next = null; + if (parent.Literals == null || + !parent.Literals.TryGetValue(literal, out next)) + { + next = new DfaNode() + { + PathDepth = parent.PathDepth + 1, + Label = includeLabel ? parent.Label + literal + "/" : null, + }; + parent.AddLiteral(literal, next); + } + + nextParents.Add(next); + } + private RoutePatternPathSegment GetCurrentSegment(RouteEndpoint endpoint, int depth) { if (depth < endpoint.RoutePattern.PathSegments.Count) @@ -500,11 +527,25 @@ namespace Microsoft.AspNetCore.Routing.Matching slotIndex = _assignments.Count; _assignments.Add(parameterPart.Name, slotIndex); - var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll; - _slots.Add(hasDefaultValue ? new KeyValuePair(parameterPart.Name, parameterPart.Default) : default); + // A parameter can have a required value, default value/catch all, or be a normal parameter + // Add the required value or default value as the slot's initial value + if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out var requiredValue)) + { + _slots.Add(new KeyValuePair(parameterPart.Name, requiredValue)); + } + else + { + var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll; + _slots.Add(hasDefaultValue ? new KeyValuePair(parameterPart.Name, parameterPart.Default) : default); + } } - if (parameterPart.IsCatchAll) + if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out _)) + { + // Don't capture a parameter if it has a required value + // There is no need because a parameter with a required value is matched as a literal + } + else if (parameterPart.IsCatchAll) { catchAll = (parameterPart.Name, i, slotIndex); } @@ -720,5 +761,15 @@ namespace Microsoft.AspNetCore.Routing.Matching return (nodeBuilderPolicies.ToArray(), endpointComparerPolicies.ToArray(), endpointSelectorPolicies.ToArray()); } + + private static bool TryGetRequiredValue(RoutePattern routePattern, RoutePatternParameterPart parameterPart, out object value) + { + if (!routePattern.RequiredValues.TryGetValue(parameterPart.Name, out value)) + { + return false; + } + + return !RouteValueEqualityComparer.Default.Equals(value, string.Empty); + } } } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs index b1f313f7f9..787e626211 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs @@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns updatedDefaults ?? original.Defaults, original.ParameterPolicies, requiredValues, - updatedParameters ?? original.Parameters, + updatedParameters ?? original.Parameters, updatedSegments ?? original.PathSegments); } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteNameMetadata.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteNameMetadata.cs new file mode 100644 index 0000000000..6f1a871636 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteNameMetadata.cs @@ -0,0 +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 System; +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Metadata used during link generation to find the associated endpoint using route name. + /// + [DebuggerDisplay("{DebuggerToString(),nq}")] + public sealed class RouteNameMetadata : IRouteNameMetadata + { + /// + /// Creates a new instance of with the provided route name. + /// + /// The route name. Can be null. + public RouteNameMetadata(string routeName) + { + RouteName = routeName; + } + + /// + /// Gets the route name. Can be null. + /// + public string RouteName { get; } + + internal string DebuggerToString() + { + return $"Name: {RouteName}"; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs index c901d185ee..f09ed7aab2 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Routing /// Metadata used during link generation to find the associated endpoint using route values. /// [DebuggerDisplay("{DebuggerToString(),nq}")] + [Obsolete("Route values are now specified on a RoutePattern.")] public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata { private static readonly IReadOnlyDictionary EmptyRouteValues = diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs index b31b59345e..6d77645dd1 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs @@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Routing continue; } - var metadata = endpoint.Metadata.GetMetadata(); + var metadata = endpoint.Metadata.GetMetadata(); if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0) { continue; @@ -137,8 +137,8 @@ namespace Microsoft.AspNetCore.Routing } var entry = CreateOutboundRouteEntry( - routeEndpoint, - metadata?.RequiredValues ?? routeEndpoint.RoutePattern.RequiredValues, + routeEndpoint, + routeEndpoint.RoutePattern.RequiredValues, metadata?.RouteName); var outboundMatch = new OutboundMatch() { Entry = entry }; diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs index d529c511d6..d5b3ebc03f 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs @@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Routing.Template { var segment = routePattern.PathSegments[i]; - var digit = ComputeInboundPrecedenceDigit(segment); + var digit = ComputeInboundPrecedenceDigit(routePattern, segment); Debug.Assert(digit >= 0 && digit < 10); precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); @@ -217,7 +217,9 @@ namespace Microsoft.AspNetCore.Routing.Template } // see description on ComputeInboundPrecedenceDigit(TemplateSegment segment) - private static int ComputeInboundPrecedenceDigit(RoutePatternPathSegment pathSegment) + // + // With a RoutePattern, parameters with a required value are treated as a literal segment + private static int ComputeInboundPrecedenceDigit(RoutePattern routePattern, RoutePatternPathSegment pathSegment) { if (pathSegment.Parts.Count > 1) { @@ -233,6 +235,13 @@ namespace Microsoft.AspNetCore.Routing.Template } else if (part is RoutePatternParameterPart parameterPart) { + // Parameter with a required value is matched as a literal + if (routePattern.RequiredValues.TryGetValue(parameterPart.Name, out var requiredValue) && + !RouteValueEqualityComparer.Default.Equals(requiredValue, string.Empty)) + { + return 1; + } + var digit = parameterPart.IsCatchAll ? 5 : 3; // If there is a route constraint for the parameter, reduce order by 1 diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs index 7bba419d12..36cf922a19 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -174,11 +174,23 @@ namespace Microsoft.AspNetCore.Routing.Template ambientValueProcessedCount++; } - if (hasExplicitValue) + // For now, only check ambient values with required values that don't have a parameter + // Ambient values for parameters are processed below + var hasParameter = _pattern.GetParameter(key) != null; + if (!hasParameter) { - // Note that we don't increment valueProcessedCount here. We expect required values - // to also be filters, which are tracked when we populate 'slots'. - if (!RoutePartsEqual(value, ambientValue)) + if (!_pattern.RequiredValues.TryGetValue(key, out var requiredValue)) + { + throw new InvalidOperationException($"Unable to find required value '{key}' on route pattern."); + } + + if (!RoutePartsEqual(ambientValue, _pattern.RequiredValues[key])) + { + copyAmbientValues = false; + break; + } + + if (hasExplicitValue && !RoutePartsEqual(value, ambientValue)) { copyAmbientValues = false; break; diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs index de47be3873..45533a36a7 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.DependencyInjection; using Moq; diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 9b423891d8..120c4995b1 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -309,14 +309,6 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); } - private class UpperCaseParameterTransform : IOutboundParameterTransformer - { - public string TransformOutbound(object value) - { - return value?.ToString()?.ToUpperInvariant(); - } - } - [Fact] public void GetLink_ParameterTransformer() { @@ -346,7 +338,7 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint = EndpointFactory.CreateRouteEndpoint( "{controller:upper-case}/{name}", - requiredValues: new { controller = "Home", name = "Test", c = "hithere", }, + requiredValues: new { controller = "Home", name = "Test", }, policies: new { c = new UpperCaseParameterTransform(), }); Action configure = (s) => @@ -586,24 +578,24 @@ namespace Microsoft.AspNetCore.Routing "Home/Index", order: 3, defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpointController = EndpointFactory.CreateRouteEndpoint( "Home", order: 2, defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpointEmpty = EndpointFactory.CreateRouteEndpoint( "", order: 1, defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); // This endpoint should be used to generate the link when an id is present var endpointControllerActionParameter = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", order: 0, defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var linkGenerator = CreateLinkGenerator(endpointControllerAction, endpointController, endpointEmpty, endpointControllerActionParameter); @@ -642,11 +634,11 @@ namespace Microsoft.AspNetCore.Routing var homeIndex = EndpointFactory.CreateRouteEndpoint( "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var homeLogin = EndpointFactory.CreateRouteEndpoint( "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Login", })) }); + requiredValues: new { controller = "Home", action = "Login", }); var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); @@ -685,11 +677,11 @@ namespace Microsoft.AspNetCore.Routing var homeIndex = EndpointFactory.CreateRouteEndpoint( "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var homeLogin = EndpointFactory.CreateRouteEndpoint( "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Login", })) }); + requiredValues: new { controller = "Home", action = "Login", }); var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs index 99ac7752f5..09f07b4809 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs @@ -3,9 +3,13 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Routing.TestObjects; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Routing { @@ -20,17 +24,22 @@ namespace Microsoft.AspNetCore.Routing string displayName = null, params object[] metadata) { - var d = new List(metadata ?? Array.Empty()); - if (requiredValues != null) - { - d.Add(new RouteValuesAddressMetadata(new RouteValueDictionary(requiredValues))); - } + var routePattern = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + return CreateRouteEndpoint(routePattern, order, displayName, metadata); + } + + public static RouteEndpoint CreateRouteEndpoint( + RoutePattern routePattern = null, + int order = 0, + string displayName = null, + IList metadata = null) + { return new RouteEndpoint( TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, policies), + routePattern, order, - new EndpointMetadataCollection(d), + new EndpointMetadataCollection(metadata ?? Array.Empty()), displayName); } } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs index b317929355..5f8d509d54 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs @@ -24,11 +24,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -86,11 +86,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -116,11 +116,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -145,11 +145,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -177,11 +177,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + requiredValues: new { controller = "Home", action = "Index", }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs index 2eff433602..c031bdd4f9 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs @@ -7,6 +7,8 @@ using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Routing.TestObjects; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; @@ -727,6 +729,253 @@ namespace Microsoft.AspNetCore.Routing.Matching Assert.Null(a.PolicyEdges); } + [Fact] + public void BuildDfaTree_RequiredValues() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint = CreateEndpoint("{controller}/{action}", requiredValues: new { controller = "Home", action = "Index" }); + builder.AddEndpoint(endpoint); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("Home", next.Key); + + var home = next.Value; + Assert.Null(home.Matches); + Assert.Null(home.Parameters); + + next = Assert.Single(home.Literals); + Assert.Equal("Index", next.Key); + + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } + + [Fact] + public void BuildDfaTree_RequiredValues_AndMatchingDefaults() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint = CreateEndpoint( + "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Home", action = "Index" }); + builder.AddEndpoint(endpoint); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Same(endpoint, Assert.Single(root.Matches)); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("Home", next.Key); + + var home = next.Value; + Assert.Same(endpoint, Assert.Single(home.Matches)); + Assert.Null(home.Parameters); + + next = Assert.Single(home.Literals); + Assert.Equal("Index", next.Key); + + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } + + [Fact] + public void BuildDfaTree_RequiredValues_AndDifferentDefaults() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint = CreateSubsitutedEndpoint( + "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Login", action = "Index" }); + builder.AddEndpoint(endpoint); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("Login", next.Key); + + var login = next.Value; + Assert.Same(endpoint, Assert.Single(login.Matches)); + Assert.Null(login.Parameters); + + next = Assert.Single(login.Literals); + Assert.Equal("Index", next.Key); + + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } + + [Fact] + public void BuildDfaTree_RequiredValues_Multiple() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateSubsitutedEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Home", action = "Index" }); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateSubsitutedEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Login", action = "Index" }); + builder.AddEndpoint(endpoint2); + + var endpoint3 = CreateSubsitutedEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Login", action = "ChangePassword" }); + builder.AddEndpoint(endpoint3); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Same(endpoint1, Assert.Single(root.Matches)); + Assert.Null(root.Parameters); + + Assert.Equal(2, root.Literals.Count); + + var home = root.Literals["Home"]; + + Assert.Same(endpoint1, Assert.Single(home.Matches)); + Assert.Null(home.Parameters); + + var next = Assert.Single(home.Literals); + Assert.Equal("Index", next.Key); + + var homeIndex = next.Value; + Assert.Same(endpoint1, Assert.Single(homeIndex.Matches)); + Assert.Null(homeIndex.Literals); + Assert.NotNull(homeIndex.Parameters); + + Assert.Same(endpoint1, Assert.Single(homeIndex.Parameters.Matches)); + + var login = root.Literals["Login"]; + + Assert.Same(endpoint2, Assert.Single(login.Matches)); + Assert.Null(login.Parameters); + + Assert.Equal(2, login.Literals.Count); + + var loginIndex = login.Literals["Index"]; + + Assert.Same(endpoint2, Assert.Single(loginIndex.Matches)); + Assert.Null(loginIndex.Literals); + Assert.NotNull(loginIndex.Parameters); + + Assert.Same(endpoint2, Assert.Single(loginIndex.Parameters.Matches)); + + var loginChangePassword = login.Literals["ChangePassword"]; + + Assert.Same(endpoint3, Assert.Single(loginChangePassword.Matches)); + Assert.Null(loginChangePassword.Literals); + Assert.NotNull(loginChangePassword.Parameters); + + Assert.Same(endpoint3, Assert.Single(loginChangePassword.Parameters.Matches)); + } + + [Fact] + public void BuildDfaTree_RequiredValues_AndParameterTransformer() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint = CreateEndpoint( + "{controller:slugify}/{action:slugify}", + defaults: new { controller = "RecentProducts", action = "ViewAll" }, + requiredValues: new { controller = "RecentProducts", action = "ViewAll" }); + builder.AddEndpoint(endpoint); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Same(endpoint, Assert.Single(root.Matches)); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("recent-products", next.Key); + + var home = next.Value; + Assert.Same(endpoint, Assert.Single(home.Matches)); + Assert.Null(home.Parameters); + + next = Assert.Single(home.Literals); + Assert.Equal("view-all", next.Key); + + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } + + [Fact] + public void BuildDfaTree_RequiredValues_AndDefaults_AndParameterTransformer() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint = CreateEndpoint( + "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", + requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null }); + builder.AddEndpoint(endpoint); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("ConventionalTransformerRoute", next.Key); + + var conventionalTransformerRoute = next.Value; + Assert.Null(conventionalTransformerRoute.Matches); + Assert.Null(conventionalTransformerRoute.Parameters); + + next = Assert.Single(conventionalTransformerRoute.Literals); + Assert.Equal("conventional-transformer", next.Key); + + var conventionalTransformer = next.Value; + Assert.Same(endpoint, Assert.Single(conventionalTransformer.Matches)); + + next = Assert.Single(conventionalTransformer.Literals); + Assert.Equal("Index", next.Key); + + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + + Assert.NotNull(index.Parameters); + + Assert.Same(endpoint, Assert.Single(index.Parameters.Matches)); + } + [Fact] public void CreateCandidate_JustLiterals() { @@ -971,26 +1220,70 @@ namespace Microsoft.AspNetCore.Routing.Matching private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies) { + var policyFactory = CreateParameterPolicyFactory(); var dataSource = new CompositeEndpointDataSource(Array.Empty()); return new DfaMatcherBuilder( NullLoggerFactory.Instance, - new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), Mock.Of()), + policyFactory, Mock.Of(), policies); } - private RouteEndpoint CreateEndpoint( + private static RouteEndpoint CreateSubsitutedEndpoint( string template, object defaults = null, object constraints = null, + object requiredValues = null, params object[] metadata) { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)), - 0, - new EndpointMetadataCollection(metadata), - "test"); + var routePattern = RoutePatternFactory.Parse(template, defaults, constraints); + + var policyFactory = CreateParameterPolicyFactory(); + var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory); + + routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues); + + return EndpointFactory.CreateRouteEndpoint(routePattern, metadata: metadata); + } + + public static RoutePattern CreateRoutePattern(RoutePattern routePattern, object requiredValues) + { + if (requiredValues != null) + { + var policyFactory = CreateParameterPolicyFactory(); + var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory); + + routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues); + } + + return routePattern; + } + + private static DefaultParameterPolicyFactory CreateParameterPolicyFactory() + { + var serviceCollection = new ServiceCollection(); + var policyFactory = new DefaultParameterPolicyFactory( + Options.Create(new RouteOptions + { + ConstraintMap = + { + ["slugify"] = typeof(SlugifyParameterTransformer), + ["upper-case"] = typeof(UpperCaseParameterTransform) + } + }), + serviceCollection.BuildServiceProvider()); + + return policyFactory; + } + + private static RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + object constraints = null, + object requiredValues = null, + params object[] metadata) + { + return EndpointFactory.CreateRouteEndpoint(template, defaults, constraints, requiredValues, metadata: metadata); } private class TestMetadata1 diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs index 6227bda5b1..81ab705e97 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -19,14 +21,9 @@ namespace Microsoft.AspNetCore.Routing.Matching // so we're reusing the services here. public class DfaMatcherTest { - private RouteEndpoint CreateEndpoint(string template, int order, object defaults = null, EndpointMetadataCollection metadata = null) + private RouteEndpoint CreateEndpoint(string template, int order, object defaults = null, object requiredValues = null) { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), - order, - metadata ?? EndpointMetadataCollection.Empty, - template); + return EndpointFactory.CreateRouteEndpoint(template, defaults, requiredValues: requiredValues, order: order, displayName: template); } private Matcher CreateDfaMatcher( @@ -38,7 +35,10 @@ namespace Microsoft.AspNetCore.Routing.Matching var serviceCollection = new ServiceCollection() .AddLogging() .AddOptions() - .AddRouting(); + .AddRouting(options => + { + options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); if (policies != null) { @@ -106,6 +106,267 @@ namespace Microsoft.AspNetCore.Routing.Matching Assert.Null(context.Endpoint); } + [Fact] + public async Task MatchAsync_RequireValuesAndDefaultValues_EndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller=Home}/{action=Index}/{id?}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List + { + endpoint + }); + + var matcher = CreateDfaMatcher(dataSource); + + var (httpContext, context) = CreateContext(); + httpContext.Request.Path = "/"; + + // Act + await matcher.MatchAsync(httpContext, context); + + // Assert + Assert.Same(endpoint, context.Endpoint); + + Assert.Collection( + context.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("Home", kvp.Value); + }); + } + + [Fact] + public async Task MatchAsync_RequireValuesAndDifferentPath_NoEndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller}/{action}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List + { + endpoint + }); + + var matcher = CreateDfaMatcher(dataSource); + + var (httpContext, context) = CreateContext(); + httpContext.Request.Path = "/Login/Index"; + + // Act + await matcher.MatchAsync(httpContext, context); + + // Assert + Assert.Null(context.Endpoint); + } + + [Fact] + public async Task MatchAsync_RequireValuesAndOptionalParameter_EndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller}/{action}/{id?}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List + { + endpoint + }); + + var matcher = CreateDfaMatcher(dataSource); + + var (httpContext, context) = CreateContext(); + httpContext.Request.Path = "/Home/Index/123"; + + // Act + await matcher.MatchAsync(httpContext, context); + + // Assert + Assert.Same(endpoint, context.Endpoint); + + Assert.Collection( + context.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("Home", kvp.Value); + }, + (kvp) => + { + Assert.Equal("id", kvp.Key); + Assert.Equal("123", kvp.Value); + }); + } + + [Theory] + [InlineData("/")] + [InlineData("/TestController")] + [InlineData("/TestController/TestAction")] + [InlineData("/TestController/TestAction/17")] + [InlineData("/TestController/TestAction/17/catchAll")] + public async Task MatchAsync_ShortenedPattern_EndpointMatched(string path) + { + // Arrange + var endpoint = CreateEndpoint( + "{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}", + 0, + requiredValues: new { controller = "TestController", action = "TestAction", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List + { + endpoint + }); + + var matcher = CreateDfaMatcher(dataSource); + + var (httpContext, context) = CreateContext(); + httpContext.Request.Path = path; + + // Act + await matcher.MatchAsync(httpContext, context); + + // Assert + Assert.Same(endpoint, context.Endpoint); + + Assert.Equal("TestAction", context.RouteValues["action"]); + Assert.Equal("TestController", context.RouteValues["controller"]); + Assert.Equal("17", context.RouteValues["id"]); + } + + [Fact] + public async Task MatchAsync_MultipleEndpointsWithDifferentRequiredValues_EndpointMatched() + { + // Arrange + var endpoint1 = CreateEndpoint( + "{controller}/{action}/{id?}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + var endpoint2 = CreateEndpoint( + "{controller}/{action}/{id?}", + 0, + requiredValues: new { controller = "Login", action = "Index", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List + { + endpoint1, + endpoint2 + }); + + var matcher = CreateDfaMatcher(dataSource); + + var (httpContext, context) = CreateContext(); + httpContext.Request.Path = "/Home/Index/123"; + + // Act 1 + await matcher.MatchAsync(httpContext, context); + + // Assert 1 + Assert.Same(endpoint1, context.Endpoint); + + httpContext.Request.Path = "/Login/Index/123"; + + // Act 2 + await matcher.MatchAsync(httpContext, context); + + // Assert 2 + Assert.Same(endpoint2, context.Endpoint); + } + + [Fact] + public async Task MatchAsync_ParameterTransformer_EndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", + 0, + requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List + { + endpoint + }); + + var matcher = CreateDfaMatcher(dataSource); + + var (httpContext, context) = CreateContext(); + httpContext.Request.Path = "/ConventionalTransformerRoute/conventional-transformer/Index"; + + // Act + await matcher.MatchAsync(httpContext, context); + + // Assert + Assert.Same(endpoint, context.Endpoint); + + Assert.Collection( + context.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("ConventionalTransformer", kvp.Value); + }); + } + + [Fact] + public async Task MatchAsync_DifferentDefaultCase_RouteValueUsesDefaultCase() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller}/{action=TESTACTION}/{id?}", + 0, + requiredValues: new { controller = "TestController", action = "TestAction" }); + + var dataSource = new DefaultEndpointDataSource(new List + { + endpoint + }); + + var matcher = CreateDfaMatcher(dataSource); + + var (httpContext, context) = CreateContext(); + httpContext.Request.Path = "/TestController"; + + // Act + await matcher.MatchAsync(httpContext, context); + + // Assert + Assert.Same(endpoint, context.Endpoint); + + Assert.Collection( + context.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("TESTACTION", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("TestController", kvp.Value); + }); + } + [Fact] public async Task MatchAsync_DuplicateTemplatesAndDifferentOrder_LowerOrderEndpointMatched() { diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs index ab30b0349f..9a0145038e 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs @@ -335,5 +335,24 @@ namespace Microsoft.AspNetCore.Routing.Patterns kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); } + + [Fact] + public void SubstituteRequiredValues_NullRequiredValueParameter_Fail() + { + // Arrange + var template = "PageRoute/Attribute/{page}"; + var defaults = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } } } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs index 1a237c2ad1..5c60f5e3ed 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs @@ -13,7 +13,9 @@ namespace Microsoft.AspNetCore.Routing [Fact] public void DebuggerToString_NoNameAndRequiredValues_ReturnsString() { +#pragma warning disable CS0618 // Type or member is obsolete var metadata = new RouteValuesAddressMetadata(null, new Dictionary()); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal("Name: - Required values: ", metadata.DebuggerToString()); } @@ -21,11 +23,13 @@ namespace Microsoft.AspNetCore.Routing [Fact] public void DebuggerToString_HasNameAndRequiredValues_ReturnsString() { +#pragma warning disable CS0618 // Type or member is obsolete var metadata = new RouteValuesAddressMetadata("Name!", new Dictionary { ["requiredValue1"] = "One", ["requiredValue2"] = 2, }); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal("Name: Name! - Required values: requiredValue1 = \"One\", requiredValue2 = \"2\"", metadata.DebuggerToString()); } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs index 80f79f9172..4b15a1aca3 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches() { // Arrange 1 - var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { }); + var endpoint1 = CreateEndpoint("/a", routeName: "a"); var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 }); // Act 1 @@ -93,21 +93,21 @@ namespace Microsoft.AspNetCore.Routing Assert.Same(endpoint1, actual); // Arrange 2 - var endpoint2 = CreateEndpoint("/b", metadataRequiredValues: new { }); + var endpoint2 = CreateEndpoint("/b", routeName: "b"); // Act 2 // Trigger change dynamicDataSource.AddEndpoint(endpoint2); // Arrange 2 - var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { }); + var endpoint3 = CreateEndpoint("/c", routeName: "c"); // Act 2 // Trigger change dynamicDataSource.AddEndpoint(endpoint3); // Arrange 3 - var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { }); + var endpoint4 = CreateEndpoint("/d", routeName: "d"); // Act 3 // Trigger change @@ -280,7 +280,7 @@ namespace Microsoft.AspNetCore.Routing var expected = CreateEndpoint( "api/orders/{id}", defaults: new { controller = "Orders", action = "GetById" }, - routePatternRequiredValues: new { controller = "Orders", action = "GetById" }); + metadataRequiredValues: new { controller = "Orders", action = "GetById" }); var addressScheme = CreateAddressScheme(expected); // Act @@ -346,7 +346,7 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint = EndpointFactory.CreateRouteEndpoint( "/a", - metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteValuesAddressMetadata(string.Empty), }); + metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteNameMetadata(string.Empty), }); // Act var addressScheme = CreateAddressScheme(endpoint); @@ -369,7 +369,6 @@ namespace Microsoft.AspNetCore.Routing string template, object defaults = null, object metadataRequiredValues = null, - object routePatternRequiredValues = null, int order = 0, string routeName = null, EndpointMetadataCollection metadataCollection = null) @@ -377,16 +376,16 @@ namespace Microsoft.AspNetCore.Routing if (metadataCollection == null) { var metadata = new List(); - if (!string.IsNullOrEmpty(routeName) || metadataRequiredValues != null) + if (!string.IsNullOrEmpty(routeName)) { - metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(metadataRequiredValues))); + metadata.Add(new RouteNameMetadata(routeName)); } metadataCollection = new EndpointMetadataCollection(metadata); } return new RouteEndpoint( TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues), + RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: metadataRequiredValues), order, metadataCollection, null); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs index 049263444e..b0fa518662 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Routing.Patterns; +using Xunit; namespace Microsoft.AspNetCore.Routing.Template { @@ -23,5 +24,20 @@ namespace Microsoft.AspNetCore.Routing.Template var parsed = RoutePatternFactory.Parse(template); return func(parsed); } + + [Fact] + public void InboundPrecedence_ParameterWithRequiredValue_HasPrecedence() + { + var parameterPrecedence = RoutePatternFactory.Parse( + "{controller}").InboundPrecedence; + + var requiredValueParameterPrecedence = RoutePatternFactory.Parse( + "{controller}", + defaults: null, + parameterPolicies: null, + requiredValues: new { controller = "Home" }).InboundPrecedence; + + Assert.True(requiredValueParameterPrecedence < parameterPrecedence); + } } } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs index 5817d5ae03..40de2c08cc 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs @@ -1304,7 +1304,11 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests var binder = new TemplateBinder( UrlEncoder.Default, new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - RoutePatternFactory.Parse(template), + RoutePatternFactory.Parse( + template, + defaults, + parameterPolicies: null, + requiredValues: new { area = (string)null, action = "Param", controller = "ConventionalTransformer", page = (string)null }), defaults, requiredKeys: defaults.Keys, parameterPolicies: new (string, IParameterPolicy)[] { ("param", new LengthRouteConstraint(500)), ("param", new SlugifyParameterTransformer()), }); @@ -1317,6 +1321,96 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests Assert.Equal(expected, boundTemplate); } + [Fact] + public void BindValues_AmbientAndExplicitValuesDoNotMatch_Success() + { + // Arrange + var expected = "/Travel/Flight"; + + var template = "{area}/{controller}/{action}"; + var defaults = new RouteValueDictionary(new { action = "Index" }); + var ambientValues = new RouteValueDictionary(new { area = "Travel", controller = "Rail", action = "Index" }); + var explicitValues = new RouteValueDictionary(new { controller = "Flight", action = "Index" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, + defaults, + parameterPolicies: null, + requiredValues: new { area = "Travel", action = "SomeAction", controller = "Flight", page = (string)null }), + defaults, + requiredKeys: new string[] { "area", "action", "controller", "page" }, + parameterPolicies: null); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } + + [Fact] + public void BindValues_LinkingFromPageToAController_Success() + { + // Arrange + var expected = "/LG2/SomeAction"; + + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new RouteValueDictionary(); + var ambientValues = new RouteValueDictionary(new { page = "/LGAnotherPage", id = "17" }); + var explicitValues = new RouteValueDictionary(new { controller = "LG2", action = "SomeAction" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, + defaults, + parameterPolicies: null, + requiredValues: new { area = (string)null, action = "SomeAction", controller = "LG2", page = (string)null }), + defaults, + requiredKeys: new string[] { "area", "action", "controller", "page" }, + parameterPolicies: null); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } + + [Fact] + public void BindValues_HasUnmatchingAmbientValues_Discard() + { + // Arrange + var expected = "/Admin/LG3/SomeAction?anothervalue=5"; + + var template = "Admin/LG3/SomeAction/{id?}"; + var defaults = new RouteValueDictionary(new { controller = "LG3", action = "SomeAction", area = "Admin" }); + var ambientValues = new RouteValueDictionary(new { controller = "LG1", action = "LinkToAnArea", id = "17" }); + var explicitValues = new RouteValueDictionary(new { controller = "LG3", area = "Admin", action = "SomeAction", anothervalue = "5" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, + defaults, + parameterPolicies: null, + requiredValues: new { area = "Admin", action = "SomeAction", controller = "LG3", page = (string)null }), + defaults, + requiredKeys: new string[] { "area", "action", "controller", "page" }, + parameterPolicies: null); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } + private static IInlineConstraintResolver GetInlineConstraintResolver() { var services = new ServiceCollection().AddOptions(); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/UpperCaseParameterTransform.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/UpperCaseParameterTransform.cs new file mode 100644 index 0000000000..c929313e3b --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TestObjects/UpperCaseParameterTransform.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Routing.TestObjects +{ + public class UpperCaseParameterTransform : IOutboundParameterTransformer + { + public string TransformOutbound(object value) + { + return value?.ToString()?.ToUpperInvariant(); + } + } +} diff --git a/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs b/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs index fb067a3fe4..0ee48e5c83 100644 --- a/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs +++ b/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs @@ -98,7 +98,7 @@ namespace RoutingWebSite return response.WriteAsync( "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { })); }, - new RouteValuesAddressMetadata(routeName: "WithSingleAsteriskCatchAll", requiredValues: new RouteValueDictionary())); + new RouteNameMetadata(routeName: "WithSingleAsteriskCatchAll")); routes.MapGet( "/WithDoubleAsteriskCatchAll/{**path}", (httpContext) => @@ -111,7 +111,7 @@ namespace RoutingWebSite return response.WriteAsync( "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { })); }, - new RouteValuesAddressMetadata(routeName: "WithDoubleAsteriskCatchAll", requiredValues: new RouteValueDictionary())); + new RouteNameMetadata(routeName: "WithDoubleAsteriskCatchAll")); }); app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));