diff --git a/benchmarkapps/Benchmarks/Benchmarks.csproj b/benchmarkapps/Benchmarks/Benchmarks.csproj index b85c439bc7..3c5753cc64 100644 --- a/benchmarkapps/Benchmarks/Benchmarks.csproj +++ b/benchmarkapps/Benchmarks/Benchmarks.csproj @@ -3,10 +3,11 @@ netcoreapp2.2 $(BenchmarksTargetFramework) + true - + @@ -14,7 +15,7 @@ - + diff --git a/benchmarkapps/Benchmarks/StartupUsingDispatcher.cs b/benchmarkapps/Benchmarks/StartupUsingDispatcher.cs index 1351a20689..f8c739eab1 100644 --- a/benchmarkapps/Benchmarks/StartupUsingDispatcher.cs +++ b/benchmarkapps/Benchmarks/StartupUsingDispatcher.cs @@ -5,6 +5,7 @@ using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; namespace Benchmarks @@ -31,10 +32,8 @@ namespace Benchmarks response.ContentLength = payloadLength; return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); }, - template: "/plaintext", - defaults: new RouteValueDictionary(), + routePattern: RoutePatternFactory.Parse("/plaintext"), requiredValues: new RouteValueDictionary(), - nonInlineMatchProcessorReferences: null, order: 0, metadata: EndpointMetadataCollection.Empty, displayName: "Plaintext"), diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs index fb12d507a7..ada2c02857 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.EndpointConstraints; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Routing.Matchers @@ -42,11 +43,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers } return new MatcherEndpoint( - (next) => (context) => Task.CompletedTask, - template, + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template), new RouteValueDictionary(), - new RouteValueDictionary(), - new List(), 0, new EndpointMetadataCollection(metadata), template); diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs index 6784f2b190..0483a0b5a9 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TrivialMatcher.cs @@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers } var path = httpContext.Request.Path.Value; - if (string.Equals(_endpoint.Template, path, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) { feature.Endpoint = _endpoint; feature.Values = new RouteValueDictionary(); @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers // This is here so this can be tested alongside DFA matcher. internal CandidateSet SelectCandidates(string path, ReadOnlySpan segments) { - if (string.Equals(_endpoint.Template, path, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) { return _candidates; } diff --git a/samples/DispatcherSample.Web/Startup.cs b/samples/DispatcherSample.Web/Startup.cs index 9f8747310f..4cbebd1e89 100644 --- a/samples/DispatcherSample.Web/Startup.cs +++ b/samples/DispatcherSample.Web/Startup.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; namespace DispatcherSample.Web @@ -39,10 +40,8 @@ namespace DispatcherSample.Web response.ContentLength = payloadLength; return response.Body.WriteAsync(_homePayload, 0, payloadLength); }, - "/", + RoutePatternFactory.Parse("/"), new RouteValueDictionary(), - new RouteValueDictionary(), - new List(), 0, EndpointMetadataCollection.Empty, "Home"), @@ -55,10 +54,8 @@ namespace DispatcherSample.Web response.ContentLength = payloadLength; return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); }, - "/plaintext", - new RouteValueDictionary(), - new RouteValueDictionary(), - new List(), + RoutePatternFactory.Parse("/plaintext"), + new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "Plaintext"), @@ -69,10 +66,8 @@ namespace DispatcherSample.Web response.ContentType = "text/plain"; return response.WriteAsync("WithConstraints"); }, - "/withconstraints/{id:endsWith(_001)}", + RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"), new RouteValueDictionary(), - new RouteValueDictionary(), - new List(), 0, EndpointMetadataCollection.Empty, "withconstraints"), @@ -83,10 +78,8 @@ namespace DispatcherSample.Web response.ContentType = "text/plain"; return response.WriteAsync("withoptionalconstraints"); }, - "/withoptionalconstraints/{id:endsWith(_001)?}", + RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"), new RouteValueDictionary(), - new RouteValueDictionary(), - new List(), 0, EndpointMetadataCollection.Empty, "withoptionalconstraints"), diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index cef493c3db..a52382e50d 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -86,8 +86,8 @@ namespace Microsoft.AspNetCore.Routing var templateBinder = new TemplateBinder( UrlEncoder.Default, _uriBuildingContextPool, - endpoint.ParsedTemplate, - endpoint.Defaults); + new RouteTemplate(endpoint.RoutePattern), + new RouteValueDictionary(endpoint.RoutePattern.Defaults)); var templateValuesResult = templateBinder.GetValues(ambientValues, explicitValues); if (templateValuesResult == null) @@ -110,20 +110,19 @@ namespace Microsoft.AspNetCore.Routing { throw new ArgumentNullException(nameof(routeValues)); } - - for (var i = 0; i < endpoint.MatchProcessorReferences.Count; i++) + + foreach (var kvp in endpoint.RoutePattern.Constraints) { - var matchProcessorReference = endpoint.MatchProcessorReferences[i]; - var parameter = endpoint.ParsedTemplate.GetParameter(matchProcessorReference.ParameterName); - if (parameter != null && parameter.IsOptional && !routeValues.ContainsKey(parameter.Name)) + var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok + var constraintReferences = kvp.Value; + for (var i = 0; i < constraintReferences.Count; i++) { - continue; - } - - var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference); - if (!matchProcessor.ProcessOutbound(httpContext, routeValues)) - { - return false; + var constraintReference = constraintReferences[i]; + var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference); + if (!matchProcessor.ProcessOutbound(httpContext, routeValues)) + { + return false; + } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DefaultMatchProcessorFactory.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DefaultMatchProcessorFactory.cs index f4acb68f78..d515df57da 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/DefaultMatchProcessorFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DefaultMatchProcessorFactory.cs @@ -20,145 +20,113 @@ namespace Microsoft.AspNetCore.Routing.Matchers _serviceProvider = serviceProvider; } - public override MatchProcessor Create(MatchProcessorReference matchProcessorReference) + public override MatchProcessor Create(string parameterName, IRouteConstraint value, bool optional) { - if (matchProcessorReference == null) + if (value == null) { - throw new ArgumentNullException(nameof(matchProcessorReference)); + throw new ArgumentNullException(nameof(value)); } - if (matchProcessorReference.MatchProcessor != null) + return InitializeMatchProcessor(parameterName, optional, value, argument: null); + } + + public override MatchProcessor Create(string parameterName, MatchProcessor value, bool optional) + { + if (value == null) { - return matchProcessorReference.MatchProcessor; + throw new ArgumentNullException(nameof(value)); + } + + return InitializeMatchProcessor(parameterName, optional, value, argument: null); + } + + public override MatchProcessor Create(string parameterName, string value, bool optional) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); } // Example: // {productId:regex(\d+)} // // ParameterName: productId - // ConstraintText: regex(\d+) - // ConstraintName: regex - // ConstraintArgument: \d+ + // value: regex(\d+) + // name: regex + // argument: \d+ + (var name, var argument) = Parse(value); - (var constraintName, var constraintArgument) = Parse(matchProcessorReference.ConstraintText); - - if (!_options.ConstraintMap.TryGetValue(constraintName, out var constraintType)) + if (!_options.ConstraintMap.TryGetValue(name, out var type)) { - throw new InvalidOperationException( - $"No constraint has been registered with name '{constraintName}'."); + throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound( + name, + typeof(RouteOptions), + nameof(RouteOptions.ConstraintMap))); } - var processor = ResolveMatchProcessor( - matchProcessorReference.ParameterName, - matchProcessorReference.Optional, - constraintType, - constraintArgument); - - if (processor != null) + if (typeof(MatchProcessor).IsAssignableFrom(type)) { - return processor; + var matchProcessor = (MatchProcessor)_serviceProvider.GetRequiredService(type); + return InitializeMatchProcessor(parameterName, optional, matchProcessor, argument); } - if (!typeof(IRouteConstraint).IsAssignableFrom(constraintType)) + if (typeof(IRouteConstraint).IsAssignableFrom(type)) { - throw new InvalidOperationException( - Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( - constraintType, - constraintName, - typeof(IRouteConstraint).Name)); + var constraint = DefaultInlineConstraintResolver.CreateConstraint(type, argument); + return InitializeMatchProcessor(parameterName, optional, constraint, argument); } - try - { - return CreateMatchProcessorFromRouteConstraint( - matchProcessorReference.ParameterName, - matchProcessorReference.Optional, - constraintType, - constraintArgument); - } - catch (RouteCreationException) - { - throw; - } - catch (Exception exception) - { - throw new InvalidOperationException( - $"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.", - exception); - } + var message = Resources.FormatRoutePattern_InvalidStringConstraintReference( + type, + name, + typeof(IRouteConstraint), + typeof(MatchProcessor)); + throw new InvalidOperationException(message); } - private MatchProcessor CreateMatchProcessorFromRouteConstraint( + private MatchProcessor InitializeMatchProcessor( string parameterName, bool optional, - Type constraintType, - string constraintArgument) + IRouteConstraint constraint, + string argument) + { + var matchProcessor = (MatchProcessor)new RouteConstraintMatchProcessor(parameterName, constraint); + return InitializeMatchProcessor(parameterName, optional, matchProcessor, argument); + } + + private MatchProcessor InitializeMatchProcessor( + string parameterName, + bool optional, + MatchProcessor matchProcessor, + string argument) { - var routeConstraint = DefaultInlineConstraintResolver.CreateConstraint(constraintType, constraintArgument); - var matchProcessor = new MatchProcessorReference(parameterName, routeConstraint).MatchProcessor; if (optional) { matchProcessor = new OptionalMatchProcessor(matchProcessor); } - matchProcessor.Initialize(parameterName, constraintArgument); - + matchProcessor.Initialize(parameterName, argument); return matchProcessor; } - private MatchProcessor ResolveMatchProcessor( - string parameterName, - bool optional, - Type constraintType, - string constraintArgument) + private (string name, string argument) Parse(string text) { - if (constraintType == null) + string name; + string argument; + var indexOfFirstOpenParens = text.IndexOf('('); + if (indexOfFirstOpenParens >= 0 && text.EndsWith(")", StringComparison.Ordinal)) { - throw new ArgumentNullException(nameof(constraintType)); - } - - if (!typeof(MatchProcessor).IsAssignableFrom(constraintType)) - { - // Since a constraint type could be of type IRouteConstraint, do not throw - return null; - } - - var registeredProcessor = _serviceProvider.GetRequiredService(constraintType); - if (registeredProcessor is MatchProcessor matchProcessor) - { - if (optional) - { - matchProcessor = new OptionalMatchProcessor(matchProcessor); - } - - matchProcessor.Initialize(parameterName, constraintArgument); - return matchProcessor; - } - else - { - throw new InvalidOperationException( - $"Registered constraint type '{constraintType}' is not of type '{typeof(MatchProcessor)}'."); - } - } - - private (string constraintName, string constraintArgument) Parse(string constraintText) - { - string constraintName; - string constraintArgument; - var indexOfFirstOpenParens = constraintText.IndexOf('('); - if (indexOfFirstOpenParens >= 0 && constraintText.EndsWith(")", StringComparison.Ordinal)) - { - constraintName = constraintText.Substring(0, indexOfFirstOpenParens); - constraintArgument = constraintText.Substring( + name = text.Substring(0, indexOfFirstOpenParens); + argument = text.Substring( indexOfFirstOpenParens + 1, - constraintText.Length - indexOfFirstOpenParens - 2); + text.Length - indexOfFirstOpenParens - 2); } else { - constraintName = constraintText; - constraintArgument = null; + name = text; + argument = null; } - return (constraintName, constraintArgument); + return (name, argument); } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs index 36f4b2c6fe..ab9936ed06 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherBuilder.cs @@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers for (var i = 0; i < _entries.Count; i++) { var entry = _entries[i]; - maxDepth = Math.Max(maxDepth, entry.Pattern.Segments.Count); + maxDepth = Math.Max(maxDepth, entry.RoutePattern.PathSegments.Count); work.Add((entry, new List() { root, })); } @@ -88,9 +88,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers for (var j = 0; j < parents.Count; j++) { var parent = parents[j]; - if (segment.IsSimple && segment.Parts[0].IsLiteral) + var part = segment.Parts[0]; + if (segment.IsSimple && part is RoutePatternLiteralPart literalPart) { - var literal = segment.Parts[0].Text; + var literal = literalPart.Content; if (!parent.Literals.TryGetValue(literal, out var next)) { next = new DfaNode() @@ -103,7 +104,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers nextParents.Add(next); } - else if (segment.IsSimple && segment.Parts[0].IsCatchAll) + else if (segment.IsSimple && part is RoutePatternParameterPart parameterPart && 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 @@ -134,7 +135,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers parent.CatchAll.Matches.Add(entry); } - else if (segment.IsSimple && segment.Parts[0].IsParameter) + else if (segment.IsSimple && part.IsParameter) { if (parent.Parameters == null) { @@ -182,20 +183,20 @@ namespace Microsoft.AspNetCore.Routing.Matchers return root; } - private TemplateSegment GetCurrentSegment(MatcherBuilderEntry entry, int depth) + private RoutePatternPathSegment GetCurrentSegment(MatcherBuilderEntry entry, int depth) { - if (depth < entry.Pattern.Segments.Count) + if (depth < entry.RoutePattern.PathSegments.Count) { - return entry.Pattern.Segments[depth]; + return entry.RoutePattern.PathSegments[depth]; } - if (entry.Pattern.Segments.Count == 0) + if (entry.RoutePattern.PathSegments.Count == 0) { return null; } - var lastSegment = entry.Pattern.Segments[entry.Pattern.Segments.Count - 1]; - if (lastSegment.IsSimple && lastSegment.Parts[0].IsCatchAll) + var lastSegment = entry.RoutePattern.PathSegments[entry.RoutePattern.PathSegments.Count - 1]; + if (lastSegment.IsSimple && lastSegment.Parts[0] is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll) { return lastSegment; } @@ -305,63 +306,68 @@ namespace Microsoft.AspNetCore.Routing.Matchers var captures = new List<(string parameterName, int segmentIndex, int slotIndex)>(); (string parameterName, int segmentIndex, int slotIndex) catchAll = default; - foreach (var kvp in entry.Endpoint.Defaults) + foreach (var kvp in entry.Endpoint.RoutePattern.Defaults) { assignments.Add(kvp.Key, assignments.Count); slots.Add(kvp); } - for (var i = 0; i < entry.Pattern.Segments.Count; i++) + for (var i = 0; i < entry.Endpoint.RoutePattern.PathSegments.Count; i++) { - var segment = entry.Pattern.Segments[i]; + var segment = entry.Endpoint.RoutePattern.PathSegments[i]; if (!segment.IsSimple) { continue; } - var part = segment.Parts[0]; - if (!part.IsParameter) + var parameterPart = segment.Parts[0] as RoutePatternParameterPart; + if (parameterPart == null) { continue; } - if (!assignments.TryGetValue(part.Name, out var slotIndex)) + if (!assignments.TryGetValue(parameterPart.Name, out var slotIndex)) { slotIndex = assignments.Count; - assignments.Add(part.Name, slotIndex); + assignments.Add(parameterPart.Name, slotIndex); - var hasDefaultValue = part.DefaultValue != null || part.IsCatchAll; - slots.Add(hasDefaultValue ? new KeyValuePair(part.Name, part.DefaultValue) : default); + var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll; + slots.Add(hasDefaultValue ? new KeyValuePair(parameterPart.Name, parameterPart.Default) : default); } - if (part.IsCatchAll) + if (parameterPart.IsCatchAll) { - catchAll = (part.Name, i, slotIndex); + catchAll = (parameterPart.Name, i, slotIndex); } else { - captures.Add((part.Name, i, slotIndex)); + captures.Add((parameterPart.Name, i, slotIndex)); } } var complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>(); - for (var i = 0; i < entry.Pattern.Segments.Count; i++) + for (var i = 0; i < entry.RoutePattern.PathSegments.Count; i++) { - var segment = entry.Pattern.Segments[i]; + var segment = entry.RoutePattern.PathSegments[i]; if (segment.IsSimple) { continue; } - complexSegments.Add((segment.ToRoutePatternPathSegment(), i)); + complexSegments.Add((segment, i)); } var matchProcessors = new List(); - for (var i = 0; i < entry.Endpoint.MatchProcessorReferences.Count; i++) + foreach (var kvp in entry.Endpoint.RoutePattern.Constraints) { - var reference = entry.Endpoint.MatchProcessorReferences[i]; - var processor = _matchProcessorFactory.Create(reference); - matchProcessors.Add(processor); + var parameter = entry.Endpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok + var constraintReferences = kvp.Value; + for (var i = 0; i < constraintReferences.Count; i++) + { + var constraintReference = constraintReferences[i]; + var matchProcessor = _matchProcessorFactory.Create(parameter, constraintReference); + matchProcessors.Add(matchProcessor); + } } return new Candidate( @@ -405,25 +411,25 @@ namespace Microsoft.AspNetCore.Routing.Matchers private static bool HasAdditionalRequiredSegments(MatcherBuilderEntry entry, int depth) { - for (var i = depth; i < entry.Pattern.Segments.Count; i++) + for (var i = depth; i < entry.RoutePattern.PathSegments.Count; i++) { - var segment = entry.Pattern.Segments[i]; + var segment = entry.RoutePattern.PathSegments[i]; if (!segment.IsSimple) { // Complex segments always require more processing return true; } - var part = segment.Parts[0]; - if (part.IsLiteral) + var parameterPart = segment.Parts[0] as RoutePatternParameterPart; + if (parameterPart == null) { + // It's a literal return true; } - if (!part.IsOptional && - !part.IsCatchAll && - part.DefaultValue == null && - !entry.Endpoint.Defaults.ContainsKey(part.Name)) + if (!parameterPart.IsOptional && + !parameterPart.IsCatchAll && + parameterPart.Default == null) { return true; } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatchProcessorFactory.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatchProcessorFactory.cs index 95624ea420..e21d3bd6ea 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/MatchProcessorFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/MatchProcessorFactory.cs @@ -1,10 +1,46 @@ // 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; +using Microsoft.AspNetCore.Routing.Patterns; + namespace Microsoft.AspNetCore.Routing.Matchers { internal abstract class MatchProcessorFactory { - public abstract MatchProcessor Create(MatchProcessorReference matchProcessorReference); + public abstract MatchProcessor Create(string parameterName, string value, bool optional); + + public abstract MatchProcessor Create(string parameterName, IRouteConstraint value, bool optional); + + public abstract MatchProcessor Create(string parameterName, MatchProcessor value, bool optional); + + public MatchProcessor Create(RoutePatternParameterPart parameter, RoutePatternConstraintReference reference) + { + if (reference == null) + { + throw new ArgumentNullException(nameof(reference)); + } + + Debug.Assert(reference.MatchProcessor != null || reference.Constraint != null || reference.Content != null); + + if (reference.MatchProcessor != null) + { + return Create(parameter?.Name, reference.MatchProcessor, parameter?.IsOptional ?? false); + } + + if (reference.Constraint != null) + { + return Create(parameter?.Name, reference.Constraint, parameter?.IsOptional ?? false); + } + + if (reference.Content != null) + { + return Create(parameter?.Name, reference.Content, parameter?.IsOptional ?? false); + } + + // Unreachable + throw new NotSupportedException(); + } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatchProcessorReference.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatchProcessorReference.cs deleted file mode 100644 index 6266bb22e1..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Matchers/MatchProcessorReference.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Routing.Matchers -{ - public sealed class MatchProcessorReference - { - // Example: - // api/products/{productId:regex(\d+)} - // - // ParameterName = productId - // ConstraintText = regex(\d+) - // ConstraintArgument = \d+ - - public MatchProcessorReference(string parameterName, string constraintText) - { - ParameterName = parameterName; - ConstraintText = constraintText; - } - - public MatchProcessorReference(string parameterName, bool optional, string constraintText) - { - ParameterName = parameterName; - Optional = optional; - ConstraintText = constraintText; - } - - public MatchProcessorReference(string parameterName, MatchProcessor matchProcessor) - { - ParameterName = parameterName; - MatchProcessor = matchProcessor; - } - - internal MatchProcessor MatchProcessor { get; private set; } - - internal string ConstraintText { get; private set; } - - internal string ParameterName { get; private set; } - - internal bool Optional { get; private set; } - - public MatchProcessorReference(string parameterName, IRouteConstraint routeConstraint) - : this(parameterName, new RouteConstraintMatchProcessorAdapter(parameterName, routeConstraint)) - { - } - - private class RouteConstraintMatchProcessorAdapter : MatchProcessor - { - public string ParameterName { get; private set; } - - public IRouteConstraint RouteConstraint { get; } - - public RouteConstraintMatchProcessorAdapter(string parameterName, IRouteConstraint routeConstraint) - { - ParameterName = parameterName; - RouteConstraint = routeConstraint; - } - - public override void Initialize(string parameterName, string constraintArgument) - { - } - - public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary routeValues) - { - return RouteConstraint.Match( - httpContext, - NullRouter.Instance, - ParameterName, - routeValues, - RouteDirection.IncomingRequest); - } - - public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values) - { - return RouteConstraint.Match( - httpContext, - NullRouter.Instance, - ParameterName, - values, - RouteDirection.UrlGeneration); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs index a9e3a6d3b8..d1aed77693 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs @@ -2,8 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; -using Microsoft.AspNetCore.Routing.EndpointConstraints; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; namespace Microsoft.AspNetCore.Routing.Matchers @@ -14,14 +13,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers { Endpoint = endpoint; - Precedence = RoutePrecedence.ComputeInbound(endpoint.ParsedTemplate); + Precedence = RoutePrecedence.ComputeInbound(endpoint.RoutePattern); } public MatcherEndpoint Endpoint { get; } public int Order => Endpoint.Order; - public RouteTemplate Pattern => Endpoint.ParsedTemplate; + public RoutePattern RoutePattern => Endpoint.RoutePattern; public decimal Precedence { get; } @@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers return comparison; } - return Pattern.TemplateText.CompareTo(other.Pattern.TemplateText); + return RoutePattern.RawText.CompareTo(other.RoutePattern.RawText); } public bool PriorityEquals(MatcherBuilderEntry other) diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs index 591c98bac4..4311a28ee7 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Matchers { @@ -18,25 +18,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers public MatcherEndpoint( Func invoker, - string template, - RouteValueDictionary defaults, + RoutePattern routePattern, RouteValueDictionary requiredValues, int order, EndpointMetadataCollection metadata, string displayName) - : this(invoker, template, defaults, requiredValues, new List(), order, metadata, displayName) - { - } - - public MatcherEndpoint( - Func invoker, - string template, - RouteValueDictionary defaults, - RouteValueDictionary requiredValues, - List nonInlineMatchProcessorReferences, - int order, - EndpointMetadataCollection metadata, - string displayName) : base(metadata, displayName) { if (invoker == null) @@ -44,89 +30,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers throw new ArgumentNullException(nameof(invoker)); } - if (template == null) + if (routePattern == null) { - throw new ArgumentNullException(nameof(template)); + throw new ArgumentNullException(nameof(routePattern)); } Invoker = invoker; - Order = order; - - Template = template; - ParsedTemplate = TemplateParser.Parse(template); - + RoutePattern = routePattern; RequiredValues = requiredValues; - var mergedDefaults = GetDefaults(ParsedTemplate, defaults); - Defaults = mergedDefaults; - - var mergedReferences = MergeMatchProcessorReferences(ParsedTemplate, nonInlineMatchProcessorReferences); - MatchProcessorReferences = mergedReferences.AsReadOnly(); + Order = order; } + public Func Invoker { get; } + public int Order { get; } - public Func Invoker { get; } - public string Template { get; } - public RouteValueDictionary Defaults { get; } - + // Values required by an endpoint for it to be successfully matched on link generation - public RouteValueDictionary RequiredValues { get; } + public IReadOnlyDictionary RequiredValues { get; } - // Todo: needs review - public RouteTemplate ParsedTemplate { get; } - - public IReadOnlyList MatchProcessorReferences { get; } - - // Merge inline and non inline defaults into one - private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate, RouteValueDictionary nonInlineDefaults) - { - var result = nonInlineDefaults == null ? new RouteValueDictionary() : new RouteValueDictionary(nonInlineDefaults); - - foreach (var parameter in parsedTemplate.Parameters) - { - if (parameter.DefaultValue != null) - { - if (result.ContainsKey(parameter.Name)) - { - throw new InvalidOperationException( - Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly( - parameter.Name)); - } - else - { - result.Add(parameter.Name, parameter.DefaultValue); - } - } - } - - return result; - } - - private List MergeMatchProcessorReferences( - RouteTemplate parsedTemplate, - List nonInlineReferences) - { - var matchProcessorReferences = new List(); - - if (nonInlineReferences != null) - { - matchProcessorReferences.AddRange(nonInlineReferences); - } - - foreach (var parameter in parsedTemplate.Parameters) - { - if (parameter.InlineConstraints != null) - { - foreach (var constraint in parameter.InlineConstraints) - { - matchProcessorReferences.Add( - new MatchProcessorReference( - parameter.Name, - optional: parameter.IsOptional, - constraintText: constraint.Constraint)); - } - } - } - return matchProcessorReferences; - } + public RoutePattern RoutePattern { get; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/RouteConstraintMatchProcessor.cs b/src/Microsoft.AspNetCore.Routing/Matchers/RouteConstraintMatchProcessor.cs new file mode 100644 index 0000000000..16fd9d0a84 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/RouteConstraintMatchProcessor.cs @@ -0,0 +1,61 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal class RouteConstraintMatchProcessor : MatchProcessor + { + public RouteConstraintMatchProcessor(string parameterName, IRouteConstraint constraint) + { + ParameterName = parameterName; + Constraint = constraint; + } + + public string ParameterName { get; } + + public IRouteConstraint Constraint { get; } + + public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + return Constraint.Match( + httpContext, + NullRouter.Instance, + ParameterName, + values, + RouteDirection.IncomingRequest); + } + + public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + return Constraint.Match( + httpContext, + NullRouter.Instance, + ParameterName, + values, + RouteDirection.UrlGeneration); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs index c5b1a008e1..885f77481c 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs @@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns case null: state = ParseState.End; var constraintText = text.Substring(startIndex, currentIndex - startIndex); - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); break; case ')': // Only consume a ')' token if @@ -135,18 +135,18 @@ namespace Microsoft.AspNetCore.Routing.Patterns case null: state = ParseState.End; constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); break; case ':': state = ParseState.Start; constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); startIndex = currentIndex + 1; break; case '=': state = ParseState.End; constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); break; } break; @@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns if (indexOfClosingParantheses == -1) { constraintText = text.Substring(startIndex, currentIndex - startIndex); - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); if (currentChar == ':') { @@ -190,14 +190,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns var constraintText = text.Substring(startIndex, currentIndex - startIndex); if (constraintText.Length > 0) { - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); } break; case ':': constraintText = text.Substring(startIndex, currentIndex - startIndex); if (constraintText.Length > 0) { - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); } startIndex = currentIndex + 1; break; @@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns constraintText = text.Substring(startIndex, currentIndex - startIndex); if (constraintText.Length > 0) { - constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText)); + constraints.Add(RoutePatternFactory.Constraint(constraintText)); } currentIndex--; break; diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternConstraintReference.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternConstraintReference.cs index 272282a63f..0d8c9810bc 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternConstraintReference.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternConstraintReference.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; +using Microsoft.AspNetCore.Routing.Matchers; namespace Microsoft.AspNetCore.Routing.Patterns { @@ -11,18 +12,21 @@ namespace Microsoft.AspNetCore.Routing.Patterns [DebuggerDisplay("{DebuggerToString()}")] public sealed class RoutePatternConstraintReference { - internal RoutePatternConstraintReference(string parameterName, string content) + internal RoutePatternConstraintReference(string content) { - ParameterName = parameterName; Content = content; } - internal RoutePatternConstraintReference(string parameterName, IRouteConstraint constraint) + internal RoutePatternConstraintReference(IRouteConstraint constraint) { - ParameterName = parameterName; Constraint = constraint; } + internal RoutePatternConstraintReference(MatchProcessor matchProcessor) + { + MatchProcessor = matchProcessor; + } + /// /// Gets the constraint text. /// @@ -34,9 +38,9 @@ namespace Microsoft.AspNetCore.Routing.Patterns public IRouteConstraint Constraint { get; } /// - /// Gets the parameter name associated with the constraint. + /// Gets a pre-existing that was used to construct this reference. /// - public string ParameterName { get; } + public MatchProcessor MatchProcessor { get; } private string DebuggerToString() { diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs index e653845f87..de0b11e889 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Routing.Matchers; namespace Microsoft.AspNetCore.Routing.Patterns { @@ -158,7 +159,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns { updatedConstraints.Add(kvp.Key, new List() { - Constraint(kvp.Key, kvp.Value), + Constraint(kvp.Value), }); } } @@ -450,55 +451,73 @@ namespace Microsoft.AspNetCore.Routing.Patterns return new RoutePatternParameterPart(parameterName, @default, parameterKind, constraints.ToArray()); } - public static RoutePatternConstraintReference Constraint(string parameterName, object constraint) + public static RoutePatternConstraintReference Constraint(object constraint) { // Similar to RouteConstraintBuilder if (constraint is IRouteConstraint routeConstraint) { - return ConstraintCore(parameterName, routeConstraint); + return ConstraintCore(routeConstraint); + } + else if (constraint is MatchProcessor matchProcessor) + { + return ConstraintCore(matchProcessor); } else if (constraint is string content) { - return ConstraintCore(parameterName, new RegexRouteConstraint("^(" + content + ")$")); + return ConstraintCore(new RegexRouteConstraint("^(" + content + ")$")); } else { - throw new InvalidOperationException(Resources.FormatConstraintMustBeStringOrConstraint( - parameterName, - constraint, - typeof(IRouteConstraint))); + throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference( + constraint ?? "null", + typeof(IRouteConstraint), + typeof(MatchProcessor))); } } - public static RoutePatternConstraintReference Constraint(string parameterName, IRouteConstraint constraint) + public static RoutePatternConstraintReference Constraint(IRouteConstraint constraint) { if (constraint == null) { throw new ArgumentNullException(nameof(constraint)); } - return ConstraintCore(parameterName, constraint); + return ConstraintCore(constraint); } - public static RoutePatternConstraintReference Constraint(string parameterName, string constraint) + public static RoutePatternConstraintReference Constraint(MatchProcessor matchProcessor) + { + if (matchProcessor == null) + { + throw new ArgumentNullException(nameof(matchProcessor)); + } + + return ConstraintCore(matchProcessor); + } + + public static RoutePatternConstraintReference Constraint(string constraint) { if (string.IsNullOrEmpty(constraint)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint)); } - return ConstraintCore(parameterName, constraint); + return ConstraintCore(constraint); } - - private static RoutePatternConstraintReference ConstraintCore(string parameterName, IRouteConstraint constraint) + private static RoutePatternConstraintReference ConstraintCore(string constraint) { - return new RoutePatternConstraintReference(parameterName, constraint); + return new RoutePatternConstraintReference(constraint); } - private static RoutePatternConstraintReference ConstraintCore(string parameterName, string constraint) + private static RoutePatternConstraintReference ConstraintCore(IRouteConstraint constraint) { - return new RoutePatternConstraintReference(parameterName, constraint); + return new RoutePatternConstraintReference(constraint); + } + + private static RoutePatternConstraintReference ConstraintCore(MatchProcessor matchProcessor) + { + return new RoutePatternConstraintReference(matchProcessor); } } } diff --git a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs index b3f0ee2d2e..9623f5c460 100644 --- a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs @@ -458,6 +458,62 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatConstraintMustBeStringOrConstraint(object p0, object p1, object p2) => string.Format(CultureInfo.CurrentCulture, GetString("ConstraintMustBeStringOrConstraint"), p0, p1, p2); + /// + /// Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'. + /// + internal static string RoutePattern_InvalidConstraintReference + { + get => GetString("RoutePattern_InvalidConstraintReference"); + } + + /// + /// Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'. + /// + internal static string FormatRoutePattern_InvalidConstraintReference(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidConstraintReference"), p0, p1, p2); + + /// + /// Invalid constraint '{0}' for parameter '{1}'. A constraint must be of type 'string', '{2}', or '{3}'. + /// + internal static string RoutePattern_InvalidParameterConstraintReference + { + get => GetString("RoutePattern_InvalidParameterConstraintReference"); + } + + /// + /// Invalid constraint '{0}' for parameter '{1}'. A constraint must be of type 'string', '{2}', or '{3}'. + /// + internal static string FormatRoutePattern_InvalidParameterConstraintReference(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidParameterConstraintReference"), p0, p1, p2, p3); + + /// + /// The constraint reference '{0}' could not be resolved to a type. Register the constraint type with '{1}.{2}'. + /// + internal static string RoutePattern_ConstraintReferenceNotFound + { + get => GetString("RoutePattern_ConstraintReferenceNotFound"); + } + + /// + /// The constraint reference '{0}' could not be resolved to a type. Register the constraint type with '{1}.{2}'. + /// + internal static string FormatRoutePattern_ConstraintReferenceNotFound(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_ConstraintReferenceNotFound"), p0, p1, p2); + + /// + /// Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'. + /// + internal static string RoutePattern_InvalidStringConstraintReference + { + get => GetString("RoutePattern_InvalidStringConstraintReference"); + } + + /// + /// Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'. + /// + internal static string FormatRoutePattern_InvalidStringConstraintReference(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RoutePattern_InvalidStringConstraintReference"), p0, p1, p2, p3); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Microsoft.AspNetCore.Routing/Resources.resx index f5db3ec8d0..b0a3772b2c 100644 --- a/src/Microsoft.AspNetCore.Routing/Resources.resx +++ b/src/Microsoft.AspNetCore.Routing/Resources.resx @@ -213,4 +213,16 @@ The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'. + + Invalid constraint '{0}'. A constraint must be of type 'string', '{1}', or '{2}'. + + + Invalid constraint '{0}' for parameter '{1}'. A constraint must be of type 'string', '{2}', or '{3}'. + + + The constraint reference '{0}' could not be resolved to a type. Register the constraint type with '{1}.{2}'. + + + Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs index d9add483aa..d12456817d 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs @@ -120,13 +120,13 @@ namespace Microsoft.AspNetCore.Routing { Handler = NullRouter.Instance, Order = endpoint.Order, - Precedence = RoutePrecedence.ComputeOutbound(endpoint.ParsedTemplate), - RequiredLinkValues = endpoint.RequiredValues, - RouteTemplate = endpoint.ParsedTemplate, + Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern), + RequiredLinkValues = new RouteValueDictionary(endpoint.RequiredValues), + RouteTemplate = new RouteTemplate(endpoint.RoutePattern), Data = endpoint, RouteName = routeNameMetadata?.Name, }; - entry.Defaults = endpoint.Defaults; + entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); return entry; } diff --git a/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs b/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs index 663f455923..672ceacfdc 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Linq; +using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Template { @@ -36,6 +37,24 @@ namespace Microsoft.AspNetCore.Routing.Template return precedence; } + // See description on ComputeInbound(RouteTemplate) + internal static decimal ComputeInbound(RoutePattern routePattern) + { + var precedence = 0m; + + for (var i = 0; i < routePattern.PathSegments.Count; i++) + { + var segment = routePattern.PathSegments[i]; + + var digit = ComputeInboundPrecedenceDigit(segment); + Debug.Assert(digit >= 0 && digit < 10); + + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } + + return precedence; + } + // Compute the precedence for generating a url // e.g.: /api/template == 5.5 // /api/template/{id} == 5.53 @@ -60,6 +79,26 @@ namespace Microsoft.AspNetCore.Routing.Template return precedence; } + // see description on ComputeOutbound(RouteTemplate) + internal static decimal ComputeOutbound(RoutePattern routePattern) + { + // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, + // and 4 results in a combined precedence of 2.14 (decimal). + var precedence = 0m; + + for (var i = 0; i < routePattern.PathSegments.Count; i++) + { + var segment = routePattern.PathSegments[i]; + + var digit = ComputeOutboundPrecedenceDigit(segment); + Debug.Assert(digit >= 0 && digit < 10); + + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } + + return precedence; + } + // Segments have the following order: // 5 - Literal segments // 4 - Multi-part segments && Constrained parameter segments @@ -92,6 +131,38 @@ namespace Microsoft.AspNetCore.Routing.Template } } + // See description on ComputeOutboundPrecedenceDigit(TemplateSegment segment) + private static int ComputeOutboundPrecedenceDigit(RoutePatternPathSegment pathSegment) + { + if (pathSegment.Parts.Count > 1) + { + return 4; + } + + var part = pathSegment.Parts[0]; + if (part.IsLiteral) + { + return 5; + } + else if (part is RoutePatternParameterPart parameterPart) + { + Debug.Assert(parameterPart != null); + var digit = parameterPart.IsCatchAll ? 1 : 3; + + if (parameterPart.Constraints.Count > 0) + { + digit++; + } + + return digit; + } + else + { + // Unreachable + throw new NotSupportedException(); + } + } + // Segments have the following order: // 1 - Literal segments // 2 - Constrained parameter segments / Multi-part segments @@ -127,5 +198,40 @@ namespace Microsoft.AspNetCore.Routing.Template return digit; } } + + // see description on ComputeInboundPrecedenceDigit(TemplateSegment segment) + private static int ComputeInboundPrecedenceDigit(RoutePatternPathSegment pathSegment) + { + if (pathSegment.Parts.Count > 1) + { + // Multi-part segments should appear after literal segments and along with parameter segments + return 2; + } + + var part = pathSegment.Parts[0]; + // Literal segments always go first + if (part.IsLiteral) + { + return 1; + } + else if (part is RoutePatternParameterPart parameterPart) + { + var digit = parameterPart.IsCatchAll ? 5 : 3; + + // If there is a route constraint for the parameter, reduce order by 1 + // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 + if (parameterPart.Constraints.Count > 0) + { + digit--; + } + + return digit; + } + else + { + // Unreachable + throw new NotSupportedException(); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs index c8ba0b319c..f42cbfa011 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs @@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Template RoutePatternParameterKind.Optional : RoutePatternParameterKind.Standard; - var constraints = InlineConstraints.Select(c => new RoutePatternConstraintReference(Name, c.Constraint)); + var constraints = InlineConstraints.Select(c => new RoutePatternConstraintReference(c.Constraint)); return RoutePatternFactory.ParameterPart(Name, DefaultValue, kind, constraints); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs index 6f5978af52..74a975b2cd 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.Primitives; using Xunit; @@ -147,19 +148,15 @@ namespace Microsoft.AspNetCore.Routing private MatcherEndpoint CreateEndpoint( string template, - object defaultValues = null, + object defaults = null, object requiredValues = null, int order = 0, string routeName = null) { - var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues); - var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues); - return new MatcherEndpoint( - next => (httpContext) => Task.CompletedTask, - template, - defaults, - required, + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template, defaults, constraints: null), + new RouteValueDictionary(requiredValues), order, EndpointMetadataCollection.Empty, null); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 66397121d3..ff6d0ac25c 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.EndpointFinders; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; @@ -254,12 +255,11 @@ namespace Microsoft.AspNetCore.Routing // Arrange var context = CreateRouteValuesContext(new { p1 = "abcd" }); var linkGenerator = CreateLinkGenerator(); - var matchProcessorReferences = new List(); - matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}"))); + var endpoint = CreateEndpoint( "{p1}/{p2}", new { p2 = "catchall" }, - matchProcessorReferences: matchProcessorReferences); + constraints: new { p2 = "\\d{4}" }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -279,12 +279,11 @@ namespace Microsoft.AspNetCore.Routing // Arrange var context = CreateRouteValuesContext(new { p1 = "hello", p2 = "1234" }); var linkGenerator = CreateLinkGenerator(); - var matchProcessorReferences = new List(); - matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}"))); + var endpoint = CreateEndpoint( "{p1}/{p2}", new { p2 = "catchall" }, - matchProcessorReferences); + new { p2 = new RegexRouteConstraint("\\d{4}"), }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -305,12 +304,11 @@ namespace Microsoft.AspNetCore.Routing // Arrange var context = CreateRouteValuesContext(new { p1 = "abcd" }); var linkGenerator = CreateLinkGenerator(); - var matchProcessorReferences = new List(); - matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}"))); + var endpoint = CreateEndpoint( "{p1}/{*p2}", new { p2 = "catchall" }, - matchProcessorReferences: matchProcessorReferences); + new { p2 = new RegexRouteConstraint("\\d{4}") }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -330,12 +328,11 @@ namespace Microsoft.AspNetCore.Routing // Arrange var context = CreateRouteValuesContext(new { p1 = "hello", p2 = "1234" }); var linkGenerator = CreateLinkGenerator(); - var matchProcessorReferences = new List(); - matchProcessorReferences.Add(new MatchProcessorReference("p2", new RegexRouteConstraint("\\d{4}"))); + var endpoint = CreateEndpoint( "{p1}/{*p2}", new { p2 = "catchall" }, - matchProcessorReferences); + new { p2 = new RegexRouteConstraint("\\d{4}") }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -370,11 +367,8 @@ namespace Microsoft.AspNetCore.Routing var endpoint = CreateEndpoint( "{p1}/{p2}", - defaultValues: new { p2 = "catchall" }, - matchProcessorReferences: new List - { - new MatchProcessorReference("p2", target.Object) - }); + defaults: new { p2 = "catchall" }, + constraints: new { p2 = target.Object }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -400,11 +394,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( template: "slug/Home/Store", - defaultValues: new { controller = "Home", action = "Store" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("c", constraint) - }); + defaults: new { controller = "Home", action = "Store" }, + constraints: new { c = constraint }); var context = CreateRouteValuesContext( suppliedValues: new { action = "Store" }, @@ -437,11 +428,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( template: "slug/Home/Store", - defaultValues: new { controller = "Home", action = "Store", otherthing = "17" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("c", constraint) - }); + defaults: new { controller = "Home", action = "Store", otherthing = "17" }, + constraints: new { c = constraint }); var context = CreateRouteValuesContext( suppliedValues: new { action = "Store" }, @@ -471,11 +459,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( template: "slug/{controller}/{action}", - defaultValues: new { action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("c", constraint) - }); + defaults: new { action = "Index" }, + constraints: new { c = constraint, }); var context = CreateRouteValuesContext( suppliedValues: new { controller = "Shopping" }, @@ -506,11 +491,8 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( template: "slug/Home/Store", - defaultValues: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("c", constraint) - }); + defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, + constraints: new { c = constraint, }); var context = CreateRouteValuesContext( suppliedValues: new { action = "Store", thirdthing = "13" }, @@ -537,12 +519,10 @@ namespace Microsoft.AspNetCore.Routing // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( - template: "Home/Index/{id}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("id", "int") - }); + template: "Home/Index/{id:int}", + defaults: new { controller = "Home", action = "Index" }, + constraints: new { }); + var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", id = 4 }); @@ -564,11 +544,9 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( template: "Home/Index/{id}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("id", "int") - }); + defaults: new { controller = "Home", action = "Index" }, + constraints: new {id = "int"}); + var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); @@ -590,12 +568,9 @@ namespace Microsoft.AspNetCore.Routing // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( - template: "Home/Index/{id}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("id", optional: true, "int") - }); + template: "Home/Index/{id:int?}", + defaults: new { controller = "Home", action = "Index" }, + constraints: new { }); var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", id = 98 }); @@ -617,11 +592,9 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( template: "Home/Index/{id?}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("id", optional: true, "int") - }); + defaults: new { controller = "Home", action = "Index" }, + constraints: new { id = "int" }); + var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home" }); @@ -642,12 +615,10 @@ namespace Microsoft.AspNetCore.Routing // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( - template: "Home/Index/{id}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("id", optional: true, "int") - }); + template: "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index" }, + constraints: new { id = "int" }); + var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); @@ -664,18 +635,15 @@ namespace Microsoft.AspNetCore.Routing } [Fact] - public void GetLink_InlineConstraints_CompositeInlineConstraint() + public void GetLink_InlineConstraints_MultipleInlineConstraints() { // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( - template: "Home/Index/{id}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("id", "int"), - new MatchProcessorReference("id", "range(1,20)") - }); + template: "Home/Index/{id:int:range(1,20)}", + defaults: new { controller = "Home", action = "Index" }, + constraints: new { }); + var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", id = 14 }); @@ -696,13 +664,10 @@ namespace Microsoft.AspNetCore.Routing // Arrange var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( - template: "Home/Index/{id}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("id", "int"), - new MatchProcessorReference("id", "range(1,20)") - }); + template: "Home/Index/{id:int:range(1,20)}", + defaults: new { controller = "Home", action = "Index" }, + constraints: new { }); + var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", id = 50 }); @@ -726,11 +691,9 @@ namespace Microsoft.AspNetCore.Routing var linkGenerator = CreateLinkGenerator(); var endpoint = CreateEndpoint( template: "Home/Index/{name}", - defaultValues: new { controller = "Home", action = "Index" }, - matchProcessorReferences: new List() - { - new MatchProcessorReference("name", constraint) - }); + defaults: new { controller = "Home", action = "Index" }, + constraints: new { name = constraint }); + var context = CreateRouteValuesContext( suppliedValues: new { action = "Index", controller = "Home", name = "products" }); @@ -785,50 +748,6 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/Home/Index", link); } - [Fact] - public void GetLink_OptionalParameter_ParameterPresentInValuesAndDefaults() - { - // Arrange - var endpoint = CreateEndpoint( - template: "{controller}/{action}/{name?}", - defaultValues: new { name = "default-products" }); - var linkGenerator = CreateLinkGenerator(); - var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", name = "products" }); - - // Act - var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); - - // Assert - Assert.Equal("/Home/Index/products", link); - } - - [Fact] - public void GetLink_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() - { - // Arrange - var endpoint = CreateEndpoint( - template: "{controller}/{action}/{name?}", - defaultValues: new { name = "products" }); - var linkGenerator = CreateLinkGenerator(); - var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home" }); - - // Act - var link = linkGenerator.GetLink( - httpContext: null, - new[] { endpoint }, - context.ExplicitValues, - context.AmbientValues); - - // Assert - Assert.Equal("/Home/Index", link); - } - [Fact] public void GetLink_ParameterNotPresentInTemplate_PresentInValues() { @@ -958,7 +877,7 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() { // Arrange - var endpoint = CreateEndpoint("a/{b=15}/{c?}/{d?}", defaultValues: new { }); + var endpoint = CreateEndpoint("a/{b=15}/{c?}/{d?}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( suppliedValues: new { }, @@ -985,23 +904,15 @@ namespace Microsoft.AspNetCore.Routing private MatcherEndpoint CreateEndpoint( string template, - object defaultValues = null, - object requiredValues = null, - List matchProcessorReferences = null, - int order = 0, - EndpointMetadataCollection metadata = null) + object defaults = null, + object constraints = null, + int order = 0, + EndpointMetadataCollection metadata = null) { - var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues); - var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues); - metadata = metadata ?? EndpointMetadataCollection.Empty; - matchProcessorReferences = matchProcessorReferences ?? new List(); - return new MatcherEndpoint( - next => (httpContext) => Task.CompletedTask, - template, - defaults, - required, - matchProcessorReferences, + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template, defaults, constraints), + new RouteValueDictionary(), order, metadata, null); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherBuilder.cs index ddb0b71dc8..efd9c588ce 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using static Microsoft.AspNetCore.Routing.Matchers.BarebonesMatcher; @@ -22,11 +23,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers var matchers = new InnerMatcher[_endpoints.Count]; for (var i = 0; i < _endpoints.Count; i++) { - var parsed = TemplateParser.Parse(_endpoints[i].Template); - var segments = parsed.Segments - .Select(s => s.IsSimple && s.Parts[0].IsLiteral ? s.Parts[0].Text : null) + var endpoint = _endpoints[i]; + var pathSegments = endpoint.RoutePattern.PathSegments + .Select(s => s.IsSimple && s.Parts[0] is RoutePatternLiteralPart literalPart ? literalPart.Content : null) .ToArray(); - matchers[i] = new InnerMatcher(segments, _endpoints[i]); + matchers[i] = new InnerMatcher(pathSegments, _endpoints[i]); } return new BarebonesMatcher(matchers); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DataSourceDependentMatcherTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DataSourceDependentMatcherTest.cs index e165f65cb5..9eb4fa48a7 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DataSourceDependentMatcherTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DataSourceDependentMatcherTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Xunit; @@ -35,8 +36,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var endpoint = new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, - "a/b/c", - new RouteValueDictionary(), + RoutePatternFactory.Parse("a/b/c"), new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultMatchProcessorFactoryTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultMatchProcessorFactoryTest.cs index 5207607531..bcacb1a007 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultMatchProcessorFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DefaultMatchProcessorFactoryTest.cs @@ -4,10 +4,10 @@ using System; using System.Globalization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Routing.Matchers @@ -19,11 +19,153 @@ namespace Microsoft.AspNetCore.Routing.Matchers { // Arrange var factory = GetMatchProcessorFactory(); - var matchProcessorReference = new MatchProcessorReference("id", @"notpresent(\d+)"); - // Act & Assert + // Act var exception = Assert.Throws( - () => factory.Create(matchProcessorReference)); + () => factory.Create("id", @"notpresent(\d+)", optional: false)); + + // Assert + Assert.Equal( + $"The constraint reference 'notpresent' could not be resolved to a type. " + + $"Register the constraint type with '{typeof(RouteOptions)}.{nameof(RouteOptions.ConstraintMap)}'.", + exception.Message); + } + + [Fact] + public void Create_ThrowsException_OnInvalidType() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("bad", typeof(string)); + + var services = new ServiceCollection(); + services.AddTransient(); + + var factory = GetMatchProcessorFactory(options, services); + + // Act + var exception = Assert.Throws( + () => factory.Create("id", @"bad", optional: false)); + + // Assert + Assert.Equal( + $"Invalid constraint type '{typeof(string)}' registered as 'bad'. " + + $"A constraint type must either implement '{typeof(IRouteConstraint)}', or inherit from '{typeof(MatchProcessor)}'.", + exception.Message); + } + + [Fact] + public void Create_CreatesMatchProcessor_FromRoutePattern_String() + { + // Arrange + var factory = GetMatchProcessorFactory(); + + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Standard, + constraints: new[] { RoutePatternFactory.Constraint("int"), }); + + // Act + var matchProcessor = factory.Create(parameter, parameter.Constraints[0]); + + // Assert + Assert.IsType(Assert.IsType(matchProcessor).Constraint); + } + + [Fact] + public void Create_CreatesMatchProcessor_FromRoutePattern_String_Optional() + { + // Arrange + var factory = GetMatchProcessorFactory(); + + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Optional, + constraints: new[] { RoutePatternFactory.Constraint("int"), }); + + // Act + var matchProcessor = factory.Create(parameter, parameter.Constraints[0]); + + // Assert + Assert.IsType(matchProcessor); + } + + [Fact] + public void Create_CreatesMatchProcessor_FromRoutePattern_Constraint() + { + // Arrange + var factory = GetMatchProcessorFactory(); + + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Standard, + constraints: new[] { RoutePatternFactory.Constraint(new IntRouteConstraint()), }); + + // Act + var matchProcessor = factory.Create(parameter, parameter.Constraints[0]); + + // Assert + Assert.IsType(Assert.IsType(matchProcessor).Constraint); + } + + [Fact] + public void Create_CreatesMatchProcessor_FromRoutePattern_Constraint_Optional() + { + // Arrange + var factory = GetMatchProcessorFactory(); + + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Optional, + constraints: new[] { RoutePatternFactory.Constraint(new IntRouteConstraint()), }); + + // Act + var matchProcessor = factory.Create(parameter, parameter.Constraints[0]); + + // Assert + Assert.IsType(matchProcessor); + } + + [Fact] + public void Create_CreatesMatchProcessor_FromRoutePattern_MatchProcessor() + { + // Arrange + var factory = GetMatchProcessorFactory(); + + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Standard, + constraints: new[] { RoutePatternFactory.Constraint(new EndsWithStringMatchProcessor()), }); + + // Act + var matchProcessor = factory.Create(parameter, parameter.Constraints[0]); + + // Assert + Assert.IsType(matchProcessor); + } + + [Fact] + public void Create_CreatesMatchProcessor_FromRoutePattern_MatchProcessor_Optional() + { + // Arrange + var factory = GetMatchProcessorFactory(); + + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Optional, + constraints: new[] { RoutePatternFactory.Constraint(new EndsWithStringMatchProcessor()), }); + + // Act + var matchProcessor = factory.Create(parameter, parameter.Constraints[0]); + + // Assert + Assert.IsType(matchProcessor); } [Fact] @@ -31,29 +173,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers { // Arrange var factory = GetMatchProcessorFactory(); - var matchProcessorReference = new MatchProcessorReference("id", "int"); - // Act 1 - var processor = factory.Create(matchProcessorReference); + // Act + var matchProcessor = factory.Create("id", "int", optional: false); - // Assert 1 - Assert.NotNull(processor); - - // Act 2 - var isMatch = processor.ProcessInbound( - new DefaultHttpContext(), - new RouteValueDictionary(new { id = 10 })); - - // Assert 2 - Assert.True(isMatch); - - // Act 2 - isMatch = processor.ProcessInbound( - new DefaultHttpContext(), - new RouteValueDictionary(new { id = "foo" })); - - // Assert 2 - Assert.False(isMatch); + // Assert + Assert.IsType(Assert.IsType(matchProcessor).Constraint); } [Fact] @@ -61,84 +186,50 @@ namespace Microsoft.AspNetCore.Routing.Matchers { // Arrange var factory = GetMatchProcessorFactory(); - var matchProcessorReference = new MatchProcessorReference("id", true, "int"); // Act - var processor = factory.Create(matchProcessorReference); + var matchProcessor = factory.Create("id", "int", optional: true); // Assert - Assert.IsType(processor); + Assert.IsType(matchProcessor); } [Fact] - public void Create_CreatesMatchProcessor_FromConstraintText_AndCustomMatchProcessor() + public void Create_CreatesMatchProcessor_FromConstraintText_AndMatchProcesor() { // Arrange var options = new RouteOptions(); options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor)); + var services = new ServiceCollection(); services.AddTransient(); + var factory = GetMatchProcessorFactory(options, services); - var matchProcessorReference = new MatchProcessorReference("id", "endsWith(_001)"); - // Act 1 - var processor = factory.Create(matchProcessorReference); + // Act + var matchProcessor = factory.Create("id", "endsWith", optional: false); - // Assert 1 - Assert.NotNull(processor); - - // Act 2 - var isMatch = processor.ProcessInbound( - new DefaultHttpContext(), - new RouteValueDictionary(new { id = "555_001" })); - - // Assert 2 - Assert.True(isMatch); - - // Act 2 - isMatch = processor.ProcessInbound( - new DefaultHttpContext(), - new RouteValueDictionary(new { id = "444" })); - - // Assert 2 - Assert.False(isMatch); + // Assert + Assert.IsType(matchProcessor); } [Fact] - public void Create_ReturnsMatchProcessor_IfAvailable() + public void Create_CreatesMatchProcessor_FromConstraintText_AndMatchProcessor_Optional() { // Arrange - var factory = GetMatchProcessorFactory(); - var matchProcessorReference = new MatchProcessorReference("id", Mock.Of()); - var expected = matchProcessorReference.MatchProcessor; + var options = new RouteOptions(); + options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor)); + + var services = new ServiceCollection(); + services.AddTransient(); + + var factory = GetMatchProcessorFactory(options, services); // Act - var processor = factory.Create(matchProcessorReference); + var matchProcessor = factory.Create("id", "endsWith", optional: true); // Assert - Assert.Same(expected, processor); - } - - [Fact] - public void Create_ReturnsMatchProcessor_WithSuppliedRouteConstraint() - { - // Arrange - var factory = GetMatchProcessorFactory(); - var constraint = TestRouteConstraint.Create(); - var matchProcessorReference = new MatchProcessorReference("id", constraint); - var processor = factory.Create(matchProcessorReference); - var expectedHttpContext = new DefaultHttpContext(); - var expectedValues = new RouteValueDictionary(); - - // Act - processor.ProcessInbound(expectedHttpContext, expectedValues); - - // Assert - Assert.Same(expectedHttpContext, constraint.HttpContext); - Assert.Same(expectedValues, constraint.Values); - Assert.Equal("id", constraint.RouteKey); - Assert.Equal(RouteDirection.IncomingRequest, constraint.RouteDirection); - Assert.Same(NullRouter.Instance, constraint.Route); + Assert.IsType(matchProcessor); } private DefaultMatchProcessorFactory GetMatchProcessorFactory( diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs index 5cbc613562..b8a064b59a 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilderTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.EndpointConstraints; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -575,10 +576,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers public void CreateCandidate_MatchProcessors() { // Arrange - var endpoint = CreateEndpoint("/a/b/c", matchProcessors: new MatchProcessorReference[] - { - new MatchProcessorReference("a", new IntRouteConstraint()), - }); + var endpoint = CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }); var builder = CreateDfaMatcherBuilder(); @@ -606,18 +604,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers } private MatcherEndpoint CreateEndpoint( - string template, + string template, object defaults = null, - IEnumerable matchProcessors = null) + object constraints = null) { - matchProcessors = matchProcessors ?? Array.Empty(); - return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, - template, - new RouteValueDictionary(defaults), + RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)), new RouteValueDictionary(), - matchProcessors.ToList(), 0, new EndpointMetadataCollection(Array.Empty()), "test"); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs index 7157147960..51a9d74b75 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.EndpointConstraints; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -18,14 +19,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers // so we're reusing the services here. public class DfaMatcherTest { - private MatcherEndpoint CreateEndpoint(string template, int order, object defaultValues = null, EndpointMetadataCollection metadata = null) + private MatcherEndpoint CreateEndpoint(string template, int order, object defaults = null, EndpointMetadataCollection metadata = null) { return new MatcherEndpoint( - (next) => null, - template, - new RouteValueDictionary(defaultValues), + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template, defaults, constraints: null), new RouteValueDictionary(), - new List(), order, metadata ?? EndpointMetadataCollection.Empty, template); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs index abef76e438..3b352a6ae6 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Routing.Matchers @@ -35,16 +36,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers internal static MatcherEndpoint CreateEndpoint( string template, - object defaultValues = null, + object defaults = null, + object constraints = null, int? order = null) { - var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues); return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, - template, - defaults, + RoutePatternFactory.Parse(template, defaults, constraints), new RouteValueDictionary(), - new List(), order ?? 0, EndpointMetadataCollection.Empty, "endpoint: " + template); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteConstraintMatchProcessorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteConstraintMatchProcessorTest.cs new file mode 100644 index 0000000000..4bc013baa4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteConstraintMatchProcessorTest.cs @@ -0,0 +1,62 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class RouteConstraintMatchProcessorTest + { + [Fact] + public void MatchInbound_CallsRouteConstraint() + { + // Arrange + var constraint = new Mock(); + constraint + .Setup(c => c.Match( + It.IsAny(), + NullRouter.Instance, + "test", + It.IsAny(), + RouteDirection.IncomingRequest)) + .Returns(true) + .Verifiable(); + + var matchProcessor = new RouteConstraintMatchProcessor("test", constraint.Object); + + // Act + var result = matchProcessor.ProcessInbound(new DefaultHttpContext(), new RouteValueDictionary()); + + // Assert + Assert.True(result); + constraint.Verify(); + } + + [Fact] + public void MatchOutput_CallsRouteConstraint() + { + // Arrange + var constraint = new Mock(); + constraint + .Setup(c => c.Match( + It.IsAny(), + NullRouter.Instance, + "test", + It.IsAny(), + RouteDirection.UrlGeneration)) + .Returns(true) + .Verifiable(); + + var matchProcessor = new RouteConstraintMatchProcessor("test", constraint.Object); + + // Act + var result = matchProcessor.ProcessOutbound(new DefaultHttpContext(), new RouteValueDictionary()); + + // Assert + Assert.True(result); + constraint.Verify(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs index 523a899b57..3748454b92 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.EndpointConstraints; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance); var groups = _entries - .GroupBy(e => (e.Order, e.Precedence, e.Endpoint.Template)) + .GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText)) .OrderBy(g => g.Key.Order) .ThenBy(g => g.Key.Precedence); @@ -47,16 +48,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers foreach (var group in groups) { var candidates = group.Select(e => e.Endpoint).ToArray(); + var endpoint = group.First().Endpoint; - // MatcherEndpoint.Values contains the default values parsed from the template + // RoutePattern.Defaults contains the default values parsed from the template // as well as those specified with a literal. We need to separate those // for legacy cases. - var endpoint = group.First().Endpoint; - var defaults = new RouteValueDictionary(endpoint.Defaults); - for (var i = 0; i < endpoint.ParsedTemplate.Parameters.Count; i++) + // + // To do this we re-parse the original text and compare. + var withoutDefaults = RoutePatternFactory.Parse(endpoint.RoutePattern.RawText); + var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); + for (var i = 0; i < withoutDefaults.Parameters.Count; i++) { - var parameter = endpoint.ParsedTemplate.Parameters[i]; - if (parameter.DefaultValue != null) + var parameter = withoutDefaults.Parameters[i]; + if (parameter.Default != null) { defaults.Remove(parameter.Name); } @@ -64,7 +68,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers routes.Add(new Route( new SelectorRouter(selector, candidates), - endpoint.Template, + endpoint.RoutePattern.RawText, defaults, new Dictionary(), new RouteValueDictionary(), diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs index 6f0cba2f23..3ed3162666 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; @@ -43,7 +44,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers var selector = new EndpointSelector(null, cache, NullLoggerFactory.Instance); var groups = _entries - .GroupBy(e => (e.Order, e.Precedence, e.Endpoint.Template)) + .GroupBy(e => (e.Order, e.Precedence, e.Endpoint.RoutePattern.RawText)) .OrderBy(g => g.Key.Order) .ThenBy(g => g.Key.Precedence); @@ -57,11 +58,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers // as well as those specified with a literal. We need to separate those // for legacy cases. var endpoint = group.First().Endpoint; - var defaults = new RouteValueDictionary(endpoint.Defaults); - for (var i = 0; i < endpoint.ParsedTemplate.Parameters.Count; i++) + var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); + for (var i = 0; i < endpoint.RoutePattern.Parameters.Count; i++) { - var parameter = endpoint.ParsedTemplate.Parameters[i]; - if (parameter.DefaultValue != null) + var parameter = endpoint.RoutePattern.Parameters[i]; + if (parameter.Default != null) { defaults.Remove(parameter.Name); } @@ -69,7 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers builder.MapInbound( new SelectorRouter(selector, candidates), - endpoint.ParsedTemplate, + new RouteTemplate(endpoint.RoutePattern), routeName: null, order: endpoint.Order); } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs index 34db4f7ffa..d16512d56d 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.AspNetCore.Routing.Matchers; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Routing.Patterns @@ -192,6 +194,42 @@ namespace Microsoft.AspNetCore.Routing.Patterns }); } + [Fact] + public void Pattern_ExtraConstraints_MatchProcessor() + { + // Arrange + var template = "{a}/{b}/{c}"; + var defaults = new { }; + var constraints = new { d = Mock.Of(), e = Mock.Of(), }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.Constraints.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("d", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.NotNull(c.MatchProcessor)); + }, + kvp => + { + Assert.Equal("e", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.NotNull(c.MatchProcessor)); + }); + } + [Fact] public void Pattern_CreatesConstraintFromString() { @@ -239,8 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns // Assert Assert.Equal( - "The constraint entry 'd' - '17' must have a string value or be of a type " + - "which implements 'Microsoft.AspNetCore.Routing.IRouteConstraint'.", + $"Invalid constraint '17'. A constraint must be of type 'string', '{typeof(IRouteConstraint)}', or '{typeof(MatchProcessor)}'.", ex.Message); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs index a4b4e6db63..760544801e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs @@ -340,7 +340,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns "p1", null, RoutePatternParameterKind.Standard, - Constraint("p1", constraint)))); + Constraint(constraint)))); // Act var actual = RoutePatternParser.Parse(template); @@ -741,7 +741,6 @@ namespace Microsoft.AspNetCore.Routing.Patterns public bool Equals(RoutePatternConstraintReference x, RoutePatternConstraintReference y) { return - x.ParameterName == y.ParameterName && x.Content == y.Content && x.Constraint == y.Constraint; } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs index aff35c2fa3..7ba5b92f90 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.ObjectPool; @@ -185,15 +186,12 @@ namespace Microsoft.AspNetCore.Routing private MatcherEndpoint CreateEndpoint( string template, - object defaultValues = null, + object defaults = null, object requiredValues = null, int order = 0, string routeName = null, EndpointMetadataCollection metadataCollection = null) { - var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues); - var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues); - if (metadataCollection == null) { metadataCollection = EndpointMetadataCollection.Empty; @@ -204,10 +202,9 @@ namespace Microsoft.AspNetCore.Routing } return new MatcherEndpoint( - next => (httpContext) => Task.CompletedTask, - template, - defaults, - required, + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template, defaults, constraints: null), + new RouteValueDictionary(requiredValues), order, metadataCollection, null);