From 807d4c97e3d62344e9b8bbaf947e08d0cbf8a7c4 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 16 Nov 2018 23:02:48 -0800 Subject: [PATCH] Add required values to RoutePattern (#912) --- .../RoutingServiceCollectionExtensions.cs | 6 + .../Internal/LinkGenerationDecisionTree.cs | 7 +- .../DefaultRoutePatternTransformer.cs | 227 ++++++++++++ .../Patterns/RoutePattern.cs | 26 ++ .../Patterns/RoutePatternFactory.cs | 108 +++++- .../Patterns/RoutePatternTransformer.cs | 35 ++ .../RouteValueEqualityComparer.cs | 2 + .../RouteValuesAddressScheme.cs | 18 +- .../Template/RouteTemplate.cs | 7 + .../DecisionTreeBuilderTest.cs | 8 +- .../DefaultRoutePatternTransformerTest.cs | 339 ++++++++++++++++++ .../Patterns/RoutePatternFactoryTest.cs | 83 +++++ .../RouteValuesAddressSchemeTest.cs | 58 ++- 13 files changed, 874 insertions(+), 50 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 00a127e861..ba4e156ae0 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -86,6 +87,11 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); + // + // Misc infrastructure + // + services.TryAddSingleton(); + return services; } diff --git a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs index 61a4cd1960..1561ed868c 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs @@ -119,12 +119,7 @@ namespace Microsoft.AspNetCore.Routing.Internal private class OutboundMatchClassifier : IClassifier { - public OutboundMatchClassifier() - { - ValueComparer = new RouteValueEqualityComparer(); - } - - public IEqualityComparer ValueComparer { get; private set; } + public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; public IDictionary GetCriteria(OutboundMatch item) { diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs b/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs new file mode 100644 index 0000000000..b1f313f7f9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs @@ -0,0 +1,227 @@ +// 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.Patterns +{ + internal class DefaultRoutePatternTransformer : RoutePatternTransformer + { + private readonly ParameterPolicyFactory _policyFactory; + + public DefaultRoutePatternTransformer(ParameterPolicyFactory policyFactory) + { + if (policyFactory == null) + { + throw new ArgumentNullException(nameof(policyFactory)); + } + + _policyFactory = policyFactory; + } + + public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues) + { + if (original == null) + { + throw new ArgumentNullException(nameof(original)); + } + + return SubstituteRequiredValuesCore(original, new RouteValueDictionary(requiredValues)); + } + + private RoutePattern SubstituteRequiredValuesCore(RoutePattern original, RouteValueDictionary requiredValues) + { + // Process each required value in sequence. Bail if we find any rejection criteria. The goal + // of rejection is to avoid creating RoutePattern instances that can't *ever* match. + // + // If we succeed, then we need to create a new RoutePattern with the provided required values. + // + // Substitution can merge with existing RequiredValues already on the RoutePattern as long + // as all of the success criteria are still met at the end. + foreach (var kvp in requiredValues) + { + // There are three possible cases here: + // 1. Required value is null-ish + // 2. Required value corresponds to a parameter + // 3. Required value corresponds to a matching default value + // + // If none of these are true then we can reject this substitution. + RoutePatternParameterPart parameter; + if (RouteValueEqualityComparer.Default.Equals(kvp.Value, string.Empty)) + { + // 1. Required value is null-ish - check to make sure that this route doesn't have a + // parameter or filter-like default. + + if (original.GetParameter(kvp.Key) != null) + { + // Fail: we can't 'require' that a parameter be null. In theory this would be possible + // for an optional parameter, but that's not really in line with the usage of this feature + // so we don't handle it. + // + // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = "" } + return null; + } + else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && + !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) + { + // Fail: this route has a non-parameter default that doesn't match. + // + // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = "" } + return null; + } + + // Success: (for this parameter at least) + // + // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... } + continue; + } + else if ((parameter = original.GetParameter(kvp.Key)) != null) + { + // 2. Required value corresponds to a parameter - check to make sure that this value matches + // any IRouteConstraint implementations. + if (!MatchesConstraints(original, parameter, kvp.Key, requiredValues)) + { + // Fail: this route has a constraint that failed. + // + // Ex: Admin/{controller:regex(Home|Login)}/{action=Index}/{id?} - with required values: { controller = "Store" } + return null; + } + + // Success: (for this parameter at least) + // + // Ex: {area}/{controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... } + continue; + } + else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && + RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) + { + // 3. Required value corresponds to a matching default value - check to make sure that this value matches + // any IRouteConstraint implementations. It's unlikely that this would happen in practice but it doesn't + // hurt for us to check. + if (!MatchesConstraints(original, parameter: null, kvp.Key, requiredValues)) + { + // Fail: this route has a constraint that failed. + // + // Ex: + // Admin/Home/{action=Index}/{id?} + // defaults: { area = "Admin" } + // constraints: { area = "Blog" } + // with required values: { area = "Admin" } + return null; + } + + // Success: (for this parameter at least) + // + // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Admin", ... } + continue; + } + else + { + // Fail: this is a required value for a key that doesn't appear in the templates, or the route + // pattern has a different default value for a non-parameter. + // + // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Blog", ... } + // OR (less likely) + // Ex: Admin/{controller=Home}/{action=Index}/{id?} with required values: { page = "/Index", ... } + return null; + } + } + + List updatedParameters = null; + List updatedSegments = null; + RouteValueDictionary updatedDefaults = null; + + // So if we get here, we're ready to update the route pattern. We need to update two things: + // 1. Remove any default values that conflict with the required values. + // 2. Merge any existing required values + foreach (var kvp in requiredValues) + { + var parameter = original.GetParameter(kvp.Key); + + // We only need to handle the case where the required value maps to a parameter. That's the only + // case where we allow a default and a required value to disagree, and we already validated the + // other cases. + if (parameter != null && + original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && + !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) + { + if (updatedDefaults == null && updatedSegments == null && updatedParameters == null) + { + updatedDefaults = new RouteValueDictionary(original.Defaults); + updatedSegments = new List(original.PathSegments); + updatedParameters = new List(original.Parameters); + } + + updatedDefaults.Remove(kvp.Key); + RemoveParameterDefault(updatedSegments, updatedParameters, parameter); + } + } + + foreach (var kvp in original.RequiredValues) + { + requiredValues.TryAdd(kvp.Key, kvp.Value); + } + + return new RoutePattern( + original.RawText, + updatedDefaults ?? original.Defaults, + original.ParameterPolicies, + requiredValues, + updatedParameters ?? original.Parameters, + updatedSegments ?? original.PathSegments); + } + + private bool MatchesConstraints(RoutePattern pattern, RoutePatternParameterPart parameter, string key, RouteValueDictionary requiredValues) + { + if (pattern.ParameterPolicies.TryGetValue(key, out var policies)) + { + for (var i = 0; i < policies.Count; i++) + { + var policy = _policyFactory.Create(parameter, policies[i]); + if (policy is IRouteConstraint constraint) + { + if (!constraint.Match(httpContext: null, NullRouter.Instance, key, requiredValues, RouteDirection.IncomingRequest)) + { + return false; + } + } + } + } + + return true; + } + + private void RemoveParameterDefault(List segments, List parameters, RoutePatternParameterPart parameter) + { + // We know that a parameter can only appear once, so we only need to rewrite one segment and one parameter. + for (var i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) + { + if (object.ReferenceEquals(parameter, segment.Parts[j])) + { + // Found it! + var updatedParameter = RoutePatternFactory.ParameterPart(parameter.Name, @default: null, parameter.ParameterKind, parameter.ParameterPolicies); + + var updatedParts = new List(segment.Parts); + updatedParts[j] = updatedParameter; + segments[i] = RoutePatternFactory.Segment(updatedParts); + + for (var k = 0; k < parameters.Count; k++) + { + if (ReferenceEquals(parameter, parameters[k])) + { + parameters[k] = updatedParameter; + break; + } + } + + return; + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs index 6b277f98cd..6852140bad 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs @@ -23,17 +23,20 @@ namespace Microsoft.AspNetCore.Routing.Patterns string rawText, IReadOnlyDictionary defaults, IReadOnlyDictionary> parameterPolicies, + IReadOnlyDictionary requiredValues, IReadOnlyList parameters, IReadOnlyList pathSegments) { Debug.Assert(defaults != null); Debug.Assert(parameterPolicies != null); Debug.Assert(parameters != null); + Debug.Assert(requiredValues != null); Debug.Assert(pathSegments != null); RawText = rawText; Defaults = defaults; ParameterPolicies = parameterPolicies; + RequiredValues = requiredValues; Parameters = parameters; PathSegments = pathSegments; @@ -53,6 +56,29 @@ namespace Microsoft.AspNetCore.Routing.Patterns /// public IReadOnlyDictionary> ParameterPolicies { get; } + /// + /// Gets a collection of route values that must be provided for this route pattern to be considered + /// applicable. + /// + /// + /// + /// allows a framework to substitute route values into a parameterized template + /// so that the same route template specification can be used to create multiple route patterns. + /// + /// This example shows how a route template can be used with required values to substitute known + /// route values for parameters. + /// + /// Route Template: "{controller=Home}/{action=Index}/{id?}" + /// Route Values: { controller = "Store", action = "Index" } + /// + /// + /// A route pattern produced in this way will match and generate URL paths like: /Store, + /// /Store/Index, and /Store/Index/17. + /// + /// + /// + public IReadOnlyDictionary RequiredValues { get; } + /// /// Gets the precedence value of the route pattern for URL matching. /// diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs index 2f755a08e9..7b577e25bd 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns /// public static class RoutePatternFactory { - private static readonly IReadOnlyDictionary EmptyDefaultsDictionary = + private static readonly IReadOnlyDictionary EmptyDictionary = new ReadOnlyDictionary(new Dictionary()); private static readonly IReadOnlyDictionary> EmptyPoliciesDictionary = @@ -61,7 +61,37 @@ namespace Microsoft.AspNetCore.Routing.Patterns } var original = RoutePatternParser.Parse(pattern); - return Pattern(original.RawText, defaults, parameterPolicies, original.PathSegments); + return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), requiredValues: null, original.PathSegments); + } + + /// + /// Creates a from its string representation along + /// with provided default values and parameter policies. + /// + /// The route pattern string to parse. + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// + /// + /// Route values that can be substituted for parameters in the route pattern. See remarks on . + /// + /// The . + public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies, object requiredValues) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + var original = RoutePatternParser.Parse(pattern); + return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), Wrap(requiredValues), original.PathSegments); } /// @@ -76,7 +106,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(null, null, null, segments); + return PatternCore(null, null, null, null, segments); } /// @@ -92,7 +122,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(rawText, null, null, segments); + return PatternCore(rawText, null, null, null, segments); } /// @@ -121,14 +151,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); + return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); } /// /// Creates a from a collection of segments along /// with provided default values and parameter policies. /// - /// The raw text to associate with the route pattern. + /// The raw text to associate with the route pattern. May be null. /// /// Additional default values to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using @@ -152,7 +182,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); + return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); } /// @@ -167,7 +197,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(null, null, null, segments); + return PatternCore(null, null, null, requiredValues: null, segments); } /// @@ -183,7 +213,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(rawText, null, null, segments); + return PatternCore(rawText, null, null, requiredValues: null, segments); } /// @@ -212,7 +242,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); + return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); } /// @@ -243,13 +273,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(segments)); } - return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); + return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); } private static RoutePattern PatternCore( string rawText, RouteValueDictionary defaults, RouteValueDictionary parameterPolicies, + RouteValueDictionary requiredValues, IEnumerable segments) { // We want to merge the segment data with the 'out of line' defaults and parameter policies. @@ -311,12 +342,56 @@ namespace Microsoft.AspNetCore.Routing.Patterns } } + // Each Required Value either needs to either: + // 1. be null-ish + // 2. have a corresponding parameter + // 3. have a corrsponding default that matches both key and value + if (requiredValues != null) + { + foreach (var kvp in requiredValues) + { + // 1.be null-ish + var found = RouteValueEqualityComparer.Default.Equals(string.Empty, kvp.Value); + + // 2. have a corresponding parameter + if (!found && parameters != null) + { + for (var i = 0; i < parameters.Count; i++) + { + if (string.Equals(kvp.Key, parameters[i].Name, StringComparison.OrdinalIgnoreCase)) + { + found = true; + break; + } + } + } + + // 3. have a corrsponding default that matches both key and value + if (!found && + updatedDefaults != null && + updatedDefaults.TryGetValue(kvp.Key, out var defaultValue) && + RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) + { + found = true; + } + + if (!found) + { + throw new InvalidOperationException( + $"No corresponding parameter or default value could be found for the required value " + + $"'{kvp.Key}={kvp.Value}'. A non-null required value must correspond to a route parameter or the " + + $"route pattern must have a matching default value."); + } + } + } + return new RoutePattern( rawText, - updatedDefaults ?? EmptyDefaultsDictionary, + updatedDefaults ?? EmptyDictionary, updatedParameterPolicies != null ? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()) : EmptyPoliciesDictionary, + requiredValues ?? EmptyDictionary, (IReadOnlyList)parameters ?? Array.Empty(), updatedSegments); @@ -449,7 +524,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(parts)); } - return SegmentCore((RoutePatternPart[]) parts.Clone()); + return SegmentCore((RoutePatternPart[])parts.Clone()); } private static RoutePatternPathSegment SegmentCore(RoutePatternPart[] parts) @@ -670,7 +745,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns parameterName: parameterName, @default: @default, parameterKind: parameterKind, - parameterPolicies: (RoutePatternParameterPolicyReference[]) parameterPolicies.Clone()); + parameterPolicies: (RoutePatternParameterPolicyReference[])parameterPolicies.Clone()); } private static RoutePatternParameterPart ParameterPartCore( @@ -802,5 +877,10 @@ namespace Microsoft.AspNetCore.Routing.Patterns { return new RoutePatternParameterPolicyReference(parameterPolicy); } + + private static RouteValueDictionary Wrap(object values) + { + return values == null ? null : new RouteValueDictionary(values); + } } } diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs new file mode 100644 index 0000000000..bea4c610fc --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs @@ -0,0 +1,35 @@ +// 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.Patterns +{ + /// + /// A singleton service that provides transformations on . + /// + public abstract class RoutePatternTransformer + { + /// + /// Attempts to substitute the provided into the provided + /// . + /// + /// The original . + /// The required values to substitute. + /// + /// A new if substitution succeeds, otherwise null. + /// + /// + /// + /// Substituting required values into a route pattern is intended for us with a general-purpose + /// parameterize route specification that can match many logical endpoints. Calling + /// can produce a derived route pattern + /// for each set of route values that corresponds to an endpoint. + /// + /// + /// The substitution process considers default values and implementations + /// when examining a required value. will + /// return null if any required value cannot be substituted. + /// + /// + public abstract RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues); + } +} diff --git a/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs b/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs index 6f2a1eab45..f7cf0570c9 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs @@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Routing /// public class RouteValueEqualityComparer : IEqualityComparer { + public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer(); + /// public new bool Equals(object x, object y) { diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs index df502c7783..b31b59345e 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs @@ -125,7 +125,8 @@ namespace Microsoft.AspNetCore.Routing continue; } - if (endpoint.Metadata.GetMetadata() == null) + var metadata = endpoint.Metadata.GetMetadata(); + if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0) { continue; } @@ -135,7 +136,10 @@ namespace Microsoft.AspNetCore.Routing continue; } - var entry = CreateOutboundRouteEntry(routeEndpoint); + var entry = CreateOutboundRouteEntry( + routeEndpoint, + metadata?.RequiredValues ?? routeEndpoint.RoutePattern.RequiredValues, + metadata?.RouteName); var outboundMatch = new OutboundMatch() { Entry = entry }; allOutboundMatches.Add(outboundMatch); @@ -156,18 +160,20 @@ namespace Microsoft.AspNetCore.Routing return (allOutboundMatches, namedOutboundMatchResults); } - private OutboundRouteEntry CreateOutboundRouteEntry(RouteEndpoint endpoint) + private OutboundRouteEntry CreateOutboundRouteEntry( + RouteEndpoint endpoint, + IReadOnlyDictionary requiredValues, + string routeName) { - var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata(); var entry = new OutboundRouteEntry() { Handler = NullRouter.Instance, Order = endpoint.Order, Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern), - RequiredLinkValues = new RouteValueDictionary(routeValuesAddressMetadata?.RequiredValues), + RequiredLinkValues = new RouteValueDictionary(requiredValues), RouteTemplate = new RouteTemplate(endpoint.RoutePattern), Data = endpoint, - RouteName = routeValuesAddressMetadata?.RouteName, + RouteName = routeName, }; entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); return entry; diff --git a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs index ff41bf6af7..027020a02d 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs @@ -16,6 +16,13 @@ namespace Microsoft.AspNetCore.Routing.Template public RouteTemplate(RoutePattern other) { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + // RequiredValues will be ignored. RouteTemplate doesn't support them. + TemplateText = other.RawText; Segments = new List(other.PathSegments.Select(p => new TemplateSegment(p))); Parameters = new List(); diff --git a/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs index 5e28fd8917..b8eea95253 100644 --- a/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs @@ -207,13 +207,7 @@ namespace Microsoft.AspNetCore.Routing.DecisionTree private class ItemClassifier : IClassifier { - public IEqualityComparer ValueComparer - { - get - { - return new RouteValueEqualityComparer(); - } - } + public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; public IDictionary GetCriteria(Item item) { diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs new file mode 100644 index 0000000000..ab30b0349f --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs @@ -0,0 +1,339 @@ +// 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.Routing.Constraints; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Patterns +{ + public class DefaultRoutePatternTransformerTest + { + public DefaultRoutePatternTransformerTest() + { + var services = new ServiceCollection(); + services.AddRouting(); + services.AddOptions(); + Transformer = services.BuildServiceProvider().GetRequiredService(); + } + + public RoutePatternTransformer Transformer { get; } + + [Fact] + public void SubstituteRequiredValues_CanAcceptNullForAnyKey() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { a = (string)null, b = "", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("a", null), kvp), + kvp => Assert.Equal(new KeyValuePair("b", string.Empty), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_RejectsNullForParameter() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = string.Empty, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_RejectsNullForOutOfLineDefault() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { area = "Admin" }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { area = string.Empty, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter() + { + // Arrange + var template = "{controller}/{action}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter_WithSameDefault() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + + // We should not need to rewrite anything in this case. + Assert.Same(actual.Defaults, original.Defaults); + Assert.Same(actual.Parameters, original.Parameters); + Assert.Same(actual.PathSegments, original.PathSegments); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter_WithDifferentDefault() + { + // Arrange + var template = "{controller=Blog}/{action=ReadPost}/{id?}"; + var defaults = new { area = "Admin", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { area = "Admin", controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + + // We should not need to rewrite anything in this case. + Assert.NotSame(actual.Defaults, original.Defaults); + Assert.NotSame(actual.Parameters, original.Parameters); + Assert.NotSame(actual.PathSegments, original.PathSegments); + + // other defaults were wiped out + Assert.Equal(new KeyValuePair("area", "Admin"), Assert.Single(actual.Defaults)); + Assert.Null(actual.GetParameter("controller").Default); + Assert.False(actual.Defaults.ContainsKey("controller")); + Assert.Null(actual.GetParameter("action").Default); + Assert.False(actual.Defaults.ContainsKey("action")); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter_WithMatchingConstraint() + { + // Arrange + var template = "{controller}/{action}/{id?}"; + var defaults = new { }; + var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanRejectValueForParameter_WithNonMatchingConstraint() + { + // Arrange + var template = "{controller}/{action}/{id?}"; + var defaults = new { }; + var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Blog", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanRejectValueForDefault_WithDifferentValue() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Blog", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_Null() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = (string)null, action = "", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = string.Empty, action = (string)null, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", null), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", ""), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_WithMatchingConstraint() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { controller = "Home", }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanRejectValueForDefault_WithSameValue_WithNonMatchingConstraint() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { controller = "Home", }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanMergeExistingRequiredValues() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { area = "Admin", controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies, new { area = "Admin", controller = "Home", }); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs index b56465dd7a..ef4ac2fc82 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs @@ -441,6 +441,89 @@ namespace Microsoft.AspNetCore.Routing.Patterns Assert.Null(paramPartD.Default); } + [Fact] + public void Parse_WithRequiredValues() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { area = "Admin", }; + var policies = new { }; + var requiredValues = new { area = "Admin", controller = "Store", action = "Index", }; + + // Act + var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + + // Assert + Assert.Collection( + action.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, + kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("Admin", kvp.Value); }, + kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); + } + + [Fact] + public void Parse_WithRequiredValues_AllowsNullRequiredValue() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + var requiredValues = new { area = (string)null, controller = "Store", action = "Index", }; + + // Act + var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + + // Assert + Assert.Collection( + action.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, + kvp => { Assert.Equal("area", kvp.Key); Assert.Null(kvp.Value); }, + kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); + } + + [Fact] + public void Parse_WithRequiredValues_AllowsEmptyRequiredValue() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + var requiredValues = new { area = "", controller = "Store", action = "Index", }; + + // Act + var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + + // Assert + Assert.Collection( + action.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, + kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("", kvp.Value); }, + kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); + } + + [Fact] + public void Parse_WithRequiredValues_ThrowsForNonParameterNonDefault() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + var requiredValues = new { area = "Admin", controller = "Store", action = "Index", }; + + // Act + var exception = Assert.Throws(() => + { + var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + }); + + // Assert + Assert.Equal( + "No corresponding parameter or default value could be found for the required value " + + "'area=Admin'. A non-null required value must correspond to a route parameter or the " + + "route pattern must have a matching default value.", + exception.Message); + } + [Fact] public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies() { diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs index b4e724bec1..80f79f9172 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs +++ b/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", requiredValues: new { }); + var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { }); 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", requiredValues: new { }); + var endpoint2 = CreateEndpoint("/b", metadataRequiredValues: new { }); // Act 2 // Trigger change dynamicDataSource.AddEndpoint(endpoint2); // Arrange 2 - var endpoint3 = CreateEndpoint("/c", requiredValues: new { }); + var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { }); // Act 2 // Trigger change dynamicDataSource.AddEndpoint(endpoint3); // Arrange 3 - var endpoint4 = CreateEndpoint("/d", requiredValues: new { }); + var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { }); // Act 3 // Trigger change @@ -146,11 +146,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, - requiredValues: new { id = 7 }); + metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { id = 12 }, - requiredValues: new { zipCode = 3510 }); + metadataRequiredValues: new { zipCode = 3510 }); var addressScheme = CreateAddressScheme(endpoint1, endpoint2); // Act @@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, - requiredValues: new { id = 7 }); + metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { id = 12 }); @@ -198,15 +198,15 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, - requiredValues: new { id = 7 }); + metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent}/{zipCode}", defaults: new { id = 12 }, - requiredValues: new { id = 12 }); + metadataRequiredValues: new { id = 12 }); var endpoint3 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { id = 12 }, - requiredValues: new { id = 12 }); + metadataRequiredValues: new { id = 12 }); var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); // Act @@ -230,7 +230,7 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, - requiredValues: new { id = 7 }); + metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint("test"); var addressScheme = CreateAddressScheme(endpoint1, endpoint2); @@ -255,7 +255,7 @@ namespace Microsoft.AspNetCore.Routing var expected = CreateEndpoint( "api/orders/{id}", defaults: new { controller = "Orders", action = "GetById" }, - requiredValues: new { controller = "Orders", action = "GetById" }, + metadataRequiredValues: new { controller = "Orders", action = "GetById" }, routeName: "OrdersApi"); var addressScheme = CreateAddressScheme(expected); @@ -273,6 +273,29 @@ namespace Microsoft.AspNetCore.Routing Assert.Same(expected, actual); } + [Fact] + public void FindEndpoints_ReturnsEndpoint_UsingRoutePatternRequiredValues() + { + // Arrange + var expected = CreateEndpoint( + "api/orders/{id}", + defaults: new { controller = "Orders", action = "GetById" }, + routePatternRequiredValues: new { controller = "Orders", action = "GetById" }); + var addressScheme = CreateAddressScheme(expected); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress + { + ExplicitValues = new RouteValueDictionary(new { id = 10 }), + AmbientValues = new RouteValueDictionary(new { controller = "Orders", action = "GetById" }), + }); + + // Assert + var actual = Assert.Single(foundEndpoints); + Assert.Same(expected, actual); + } + [Fact] public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues() { @@ -284,7 +307,7 @@ namespace Microsoft.AspNetCore.Routing var expected = CreateEndpoint( "api/orders/{id}", defaults: new { controller = "Orders", action = "GetById" }, - requiredValues: new { controller = "Orders", action = "GetById" }, + metadataRequiredValues: new { controller = "Orders", action = "GetById" }, routeName: "OrdersApi"); var addressScheme = CreateAddressScheme(expected); @@ -345,7 +368,8 @@ namespace Microsoft.AspNetCore.Routing private RouteEndpoint CreateEndpoint( string template, object defaults = null, - object requiredValues = null, + object metadataRequiredValues = null, + object routePatternRequiredValues = null, int order = 0, string routeName = null, EndpointMetadataCollection metadataCollection = null) @@ -353,16 +377,16 @@ namespace Microsoft.AspNetCore.Routing if (metadataCollection == null) { var metadata = new List(); - if (!string.IsNullOrEmpty(routeName) || requiredValues != null) + if (!string.IsNullOrEmpty(routeName) || metadataRequiredValues != null) { - metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues))); + metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(metadataRequiredValues))); } metadataCollection = new EndpointMetadataCollection(metadata); } return new RouteEndpoint( TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), + RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues), order, metadataCollection, null);