Add required values to RoutePattern (#912)
This commit is contained in:
parent
b6a1de5676
commit
807d4c97e3
|
|
@ -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<EndpointSelector, DefaultEndpointSelector>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
||||
|
||||
//
|
||||
// Misc infrastructure
|
||||
//
|
||||
services.TryAddSingleton<RoutePatternTransformer, DefaultRoutePatternTransformer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,12 +119,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
|
||||
private class OutboundMatchClassifier : IClassifier<OutboundMatch>
|
||||
{
|
||||
public OutboundMatchClassifier()
|
||||
{
|
||||
ValueComparer = new RouteValueEqualityComparer();
|
||||
}
|
||||
|
||||
public IEqualityComparer<object> ValueComparer { get; private set; }
|
||||
public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
|
||||
|
||||
public IDictionary<string, DecisionCriterionValue> GetCriteria(OutboundMatch item)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<RoutePatternParameterPart> updatedParameters = null;
|
||||
List<RoutePatternPathSegment> 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<RoutePatternPathSegment>(original.PathSegments);
|
||||
updatedParameters = new List<RoutePatternParameterPart>(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<RoutePatternPathSegment> segments, List<RoutePatternParameterPart> 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<RoutePatternPart>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,17 +23,20 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
string rawText,
|
||||
IReadOnlyDictionary<string, object> defaults,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> parameterPolicies,
|
||||
IReadOnlyDictionary<string, object> requiredValues,
|
||||
IReadOnlyList<RoutePatternParameterPart> parameters,
|
||||
IReadOnlyList<RoutePatternPathSegment> 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
|
|||
/// </summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> ParameterPolicies { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of route values that must be provided for this route pattern to be considered
|
||||
/// applicable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="RequiredValues"/> 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.
|
||||
/// <example>
|
||||
/// This example shows how a route template can be used with required values to substitute known
|
||||
/// route values for parameters.
|
||||
/// <code>
|
||||
/// Route Template: "{controller=Home}/{action=Index}/{id?}"
|
||||
/// Route Values: { controller = "Store", action = "Index" }
|
||||
/// </code>
|
||||
///
|
||||
/// A route pattern produced in this way will match and generate URL paths like: <c>/Store</c>,
|
||||
/// <c>/Store/Index</c>, and <c>/Store/Index/17</c>.
|
||||
/// </example>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IReadOnlyDictionary<string, object> RequiredValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the precedence value of the route pattern for URL matching.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
/// </summary>
|
||||
public static class RoutePatternFactory
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, object> EmptyDefaultsDictionary =
|
||||
private static readonly IReadOnlyDictionary<string, object> EmptyDictionary =
|
||||
new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RoutePattern"/> from its string representation along
|
||||
/// with provided default values and parameter policies.
|
||||
/// </summary>
|
||||
/// <param name="pattern">The route pattern string to parse.</param>
|
||||
/// <param name="defaults">
|
||||
/// Additional default values to associated with the route pattern. May be null.
|
||||
/// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
|
||||
/// and then merged into the parsed route pattern.
|
||||
/// </param>
|
||||
/// <param name="parameterPolicies">
|
||||
/// Additional parameter policies to associated with the route pattern. May be null.
|
||||
/// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
|
||||
/// and then merged into the parsed route pattern.
|
||||
/// </param>
|
||||
/// <param name="requiredValues">
|
||||
/// Route values that can be substituted for parameters in the route pattern. See remarks on <see cref="RoutePattern.RequiredValues"/>.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="RoutePattern"/>.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RoutePattern"/> from a collection of segments along
|
||||
/// with provided default values and parameter policies.
|
||||
/// </summary>
|
||||
/// <param name="rawText">The raw text to associate with the route pattern.</param>
|
||||
/// <param name="rawText">The raw text to associate with the route pattern. May be null.</param>
|
||||
/// <param name="defaults">
|
||||
/// Additional default values to associated with the route pattern. May be null.
|
||||
/// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<RoutePatternPathSegment> 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<RoutePatternParameterPolicyReference>)kvp.Value.ToArray())
|
||||
: EmptyPoliciesDictionary,
|
||||
requiredValues ?? EmptyDictionary,
|
||||
(IReadOnlyList<RoutePatternParameterPart>)parameters ?? Array.Empty<RoutePatternParameterPart>(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A singleton service that provides transformations on <see cref="RoutePattern"/>.
|
||||
/// </summary>
|
||||
public abstract class RoutePatternTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to substitute the provided <paramref name="requiredValues"/> into the provided
|
||||
/// <paramref name="original"/>.
|
||||
/// </summary>
|
||||
/// <param name="original">The original <see cref="RoutePattern"/>.</param>
|
||||
/// <param name="requiredValues">The required values to substitute.</param>
|
||||
/// <returns>
|
||||
/// A new <see cref="RoutePattern"/> if substitution succeeds, otherwise <c>null</c>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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
|
||||
/// <see cref="SubstituteRequiredValues(RoutePattern, object)"/> can produce a derived route pattern
|
||||
/// for each set of route values that corresponds to an endpoint.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The substitution process considers default values and <see cref="IRouteConstraint"/> implementations
|
||||
/// when examining a required value. <see cref="SubstituteRequiredValues(RoutePattern, object)"/> will
|
||||
/// return <c>null</c> if any required value cannot be substituted.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Routing
|
|||
/// </remarks>
|
||||
public class RouteValueEqualityComparer : IEqualityComparer<object>
|
||||
{
|
||||
public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer();
|
||||
|
||||
/// <inheritdoc />
|
||||
public new bool Equals(object x, object y)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -125,7 +125,8 @@ namespace Microsoft.AspNetCore.Routing
|
|||
continue;
|
||||
}
|
||||
|
||||
if (endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>() == null)
|
||||
var metadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
||||
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<string, object> requiredValues,
|
||||
string routeName)
|
||||
{
|
||||
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
|
||||
Parameters = new List<TemplatePart>();
|
||||
|
|
|
|||
|
|
@ -207,13 +207,7 @@ namespace Microsoft.AspNetCore.Routing.DecisionTree
|
|||
|
||||
private class ItemClassifier : IClassifier<Item>
|
||||
{
|
||||
public IEqualityComparer<object> ValueComparer
|
||||
{
|
||||
get
|
||||
{
|
||||
return new RouteValueEqualityComparer();
|
||||
}
|
||||
}
|
||||
public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
|
||||
|
||||
public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<RoutePatternTransformer>();
|
||||
}
|
||||
|
||||
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<string, object>("a", null), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", null), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("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<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InvalidOperationException>(() =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<object>();
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue