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;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Matching;
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.AspNetCore.Routing.Tree;
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -86,6 +87,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
|
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
||||||
|
|
||||||
|
//
|
||||||
|
// Misc infrastructure
|
||||||
|
//
|
||||||
|
services.TryAddSingleton<RoutePatternTransformer, DefaultRoutePatternTransformer>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,12 +119,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
|
|
||||||
private class OutboundMatchClassifier : IClassifier<OutboundMatch>
|
private class OutboundMatchClassifier : IClassifier<OutboundMatch>
|
||||||
{
|
{
|
||||||
public OutboundMatchClassifier()
|
public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
|
||||||
{
|
|
||||||
ValueComparer = new RouteValueEqualityComparer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEqualityComparer<object> ValueComparer { get; private set; }
|
|
||||||
|
|
||||||
public IDictionary<string, DecisionCriterionValue> GetCriteria(OutboundMatch item)
|
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,
|
string rawText,
|
||||||
IReadOnlyDictionary<string, object> defaults,
|
IReadOnlyDictionary<string, object> defaults,
|
||||||
IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> parameterPolicies,
|
IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> parameterPolicies,
|
||||||
|
IReadOnlyDictionary<string, object> requiredValues,
|
||||||
IReadOnlyList<RoutePatternParameterPart> parameters,
|
IReadOnlyList<RoutePatternParameterPart> parameters,
|
||||||
IReadOnlyList<RoutePatternPathSegment> pathSegments)
|
IReadOnlyList<RoutePatternPathSegment> pathSegments)
|
||||||
{
|
{
|
||||||
Debug.Assert(defaults != null);
|
Debug.Assert(defaults != null);
|
||||||
Debug.Assert(parameterPolicies != null);
|
Debug.Assert(parameterPolicies != null);
|
||||||
Debug.Assert(parameters != null);
|
Debug.Assert(parameters != null);
|
||||||
|
Debug.Assert(requiredValues != null);
|
||||||
Debug.Assert(pathSegments != null);
|
Debug.Assert(pathSegments != null);
|
||||||
|
|
||||||
RawText = rawText;
|
RawText = rawText;
|
||||||
Defaults = defaults;
|
Defaults = defaults;
|
||||||
ParameterPolicies = parameterPolicies;
|
ParameterPolicies = parameterPolicies;
|
||||||
|
RequiredValues = requiredValues;
|
||||||
Parameters = parameters;
|
Parameters = parameters;
|
||||||
PathSegments = pathSegments;
|
PathSegments = pathSegments;
|
||||||
|
|
||||||
|
|
@ -53,6 +56,29 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> ParameterPolicies { get; }
|
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>
|
/// <summary>
|
||||||
/// Gets the precedence value of the route pattern for URL matching.
|
/// Gets the precedence value of the route pattern for URL matching.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RoutePatternFactory
|
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>());
|
new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> EmptyPoliciesDictionary =
|
private static readonly IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> EmptyPoliciesDictionary =
|
||||||
|
|
@ -61,7 +61,37 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
}
|
}
|
||||||
|
|
||||||
var original = RoutePatternParser.Parse(pattern);
|
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>
|
/// <summary>
|
||||||
|
|
@ -76,7 +106,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(segments));
|
throw new ArgumentNullException(nameof(segments));
|
||||||
}
|
}
|
||||||
|
|
||||||
return PatternCore(null, null, null, segments);
|
return PatternCore(null, null, null, null, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -92,7 +122,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(segments));
|
throw new ArgumentNullException(nameof(segments));
|
||||||
}
|
}
|
||||||
|
|
||||||
return PatternCore(rawText, null, null, segments);
|
return PatternCore(rawText, null, null, null, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -121,14 +151,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(segments));
|
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>
|
/// <summary>
|
||||||
/// Creates a <see cref="RoutePattern"/> from a collection of segments along
|
/// Creates a <see cref="RoutePattern"/> from a collection of segments along
|
||||||
/// with provided default values and parameter policies.
|
/// with provided default values and parameter policies.
|
||||||
/// </summary>
|
/// </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">
|
/// <param name="defaults">
|
||||||
/// Additional default values to associated 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 <see cref="RouteValueDictionary"/>
|
/// 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));
|
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>
|
/// <summary>
|
||||||
|
|
@ -167,7 +197,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(segments));
|
throw new ArgumentNullException(nameof(segments));
|
||||||
}
|
}
|
||||||
|
|
||||||
return PatternCore(null, null, null, segments);
|
return PatternCore(null, null, null, requiredValues: null, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -183,7 +213,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(segments));
|
throw new ArgumentNullException(nameof(segments));
|
||||||
}
|
}
|
||||||
|
|
||||||
return PatternCore(rawText, null, null, segments);
|
return PatternCore(rawText, null, null, requiredValues: null, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -212,7 +242,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(segments));
|
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>
|
/// <summary>
|
||||||
|
|
@ -243,13 +273,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(segments));
|
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(
|
private static RoutePattern PatternCore(
|
||||||
string rawText,
|
string rawText,
|
||||||
RouteValueDictionary defaults,
|
RouteValueDictionary defaults,
|
||||||
RouteValueDictionary parameterPolicies,
|
RouteValueDictionary parameterPolicies,
|
||||||
|
RouteValueDictionary requiredValues,
|
||||||
IEnumerable<RoutePatternPathSegment> segments)
|
IEnumerable<RoutePatternPathSegment> segments)
|
||||||
{
|
{
|
||||||
// We want to merge the segment data with the 'out of line' defaults and parameter policies.
|
// 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(
|
return new RoutePattern(
|
||||||
rawText,
|
rawText,
|
||||||
updatedDefaults ?? EmptyDefaultsDictionary,
|
updatedDefaults ?? EmptyDictionary,
|
||||||
updatedParameterPolicies != null
|
updatedParameterPolicies != null
|
||||||
? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray())
|
? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray())
|
||||||
: EmptyPoliciesDictionary,
|
: EmptyPoliciesDictionary,
|
||||||
|
requiredValues ?? EmptyDictionary,
|
||||||
(IReadOnlyList<RoutePatternParameterPart>)parameters ?? Array.Empty<RoutePatternParameterPart>(),
|
(IReadOnlyList<RoutePatternParameterPart>)parameters ?? Array.Empty<RoutePatternParameterPart>(),
|
||||||
updatedSegments);
|
updatedSegments);
|
||||||
|
|
||||||
|
|
@ -449,7 +524,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
throw new ArgumentNullException(nameof(parts));
|
throw new ArgumentNullException(nameof(parts));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SegmentCore((RoutePatternPart[]) parts.Clone());
|
return SegmentCore((RoutePatternPart[])parts.Clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RoutePatternPathSegment SegmentCore(RoutePatternPart[] parts)
|
private static RoutePatternPathSegment SegmentCore(RoutePatternPart[] parts)
|
||||||
|
|
@ -670,7 +745,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
parameterName: parameterName,
|
parameterName: parameterName,
|
||||||
@default: @default,
|
@default: @default,
|
||||||
parameterKind: parameterKind,
|
parameterKind: parameterKind,
|
||||||
parameterPolicies: (RoutePatternParameterPolicyReference[]) parameterPolicies.Clone());
|
parameterPolicies: (RoutePatternParameterPolicyReference[])parameterPolicies.Clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RoutePatternParameterPart ParameterPartCore(
|
private static RoutePatternParameterPart ParameterPartCore(
|
||||||
|
|
@ -802,5 +877,10 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
{
|
{
|
||||||
return new RoutePatternParameterPolicyReference(parameterPolicy);
|
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>
|
/// </remarks>
|
||||||
public class RouteValueEqualityComparer : IEqualityComparer<object>
|
public class RouteValueEqualityComparer : IEqualityComparer<object>
|
||||||
{
|
{
|
||||||
|
public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new bool Equals(object x, object y)
|
public new bool Equals(object x, object y)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>() == null)
|
var metadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
||||||
|
if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +136,10 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = CreateOutboundRouteEntry(routeEndpoint);
|
var entry = CreateOutboundRouteEntry(
|
||||||
|
routeEndpoint,
|
||||||
|
metadata?.RequiredValues ?? routeEndpoint.RoutePattern.RequiredValues,
|
||||||
|
metadata?.RouteName);
|
||||||
|
|
||||||
var outboundMatch = new OutboundMatch() { Entry = entry };
|
var outboundMatch = new OutboundMatch() { Entry = entry };
|
||||||
allOutboundMatches.Add(outboundMatch);
|
allOutboundMatches.Add(outboundMatch);
|
||||||
|
|
@ -156,18 +160,20 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
return (allOutboundMatches, namedOutboundMatchResults);
|
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()
|
var entry = new OutboundRouteEntry()
|
||||||
{
|
{
|
||||||
Handler = NullRouter.Instance,
|
Handler = NullRouter.Instance,
|
||||||
Order = endpoint.Order,
|
Order = endpoint.Order,
|
||||||
Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern),
|
Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern),
|
||||||
RequiredLinkValues = new RouteValueDictionary(routeValuesAddressMetadata?.RequiredValues),
|
RequiredLinkValues = new RouteValueDictionary(requiredValues),
|
||||||
RouteTemplate = new RouteTemplate(endpoint.RoutePattern),
|
RouteTemplate = new RouteTemplate(endpoint.RoutePattern),
|
||||||
Data = endpoint,
|
Data = endpoint,
|
||||||
RouteName = routeValuesAddressMetadata?.RouteName,
|
RouteName = routeName,
|
||||||
};
|
};
|
||||||
entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
|
entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
|
||||||
return entry;
|
return entry;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
|
|
||||||
public RouteTemplate(RoutePattern other)
|
public RouteTemplate(RoutePattern other)
|
||||||
{
|
{
|
||||||
|
if (other == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(other));
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredValues will be ignored. RouteTemplate doesn't support them.
|
||||||
|
|
||||||
TemplateText = other.RawText;
|
TemplateText = other.RawText;
|
||||||
Segments = new List<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
|
Segments = new List<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
|
||||||
Parameters = new List<TemplatePart>();
|
Parameters = new List<TemplatePart>();
|
||||||
|
|
|
||||||
|
|
@ -207,13 +207,7 @@ namespace Microsoft.AspNetCore.Routing.DecisionTree
|
||||||
|
|
||||||
private class ItemClassifier : IClassifier<Item>
|
private class ItemClassifier : IClassifier<Item>
|
||||||
{
|
{
|
||||||
public IEqualityComparer<object> ValueComparer
|
public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return new RouteValueEqualityComparer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
|
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);
|
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]
|
[Fact]
|
||||||
public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies()
|
public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
|
public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
|
||||||
{
|
{
|
||||||
// Arrange 1
|
// Arrange 1
|
||||||
var endpoint1 = CreateEndpoint("/a", requiredValues: new { });
|
var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { });
|
||||||
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
||||||
|
|
||||||
// Act 1
|
// Act 1
|
||||||
|
|
@ -93,21 +93,21 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
Assert.Same(endpoint1, actual);
|
Assert.Same(endpoint1, actual);
|
||||||
|
|
||||||
// Arrange 2
|
// Arrange 2
|
||||||
var endpoint2 = CreateEndpoint("/b", requiredValues: new { });
|
var endpoint2 = CreateEndpoint("/b", metadataRequiredValues: new { });
|
||||||
|
|
||||||
// Act 2
|
// Act 2
|
||||||
// Trigger change
|
// Trigger change
|
||||||
dynamicDataSource.AddEndpoint(endpoint2);
|
dynamicDataSource.AddEndpoint(endpoint2);
|
||||||
|
|
||||||
// Arrange 2
|
// Arrange 2
|
||||||
var endpoint3 = CreateEndpoint("/c", requiredValues: new { });
|
var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { });
|
||||||
|
|
||||||
// Act 2
|
// Act 2
|
||||||
// Trigger change
|
// Trigger change
|
||||||
dynamicDataSource.AddEndpoint(endpoint3);
|
dynamicDataSource.AddEndpoint(endpoint3);
|
||||||
|
|
||||||
// Arrange 3
|
// Arrange 3
|
||||||
var endpoint4 = CreateEndpoint("/d", requiredValues: new { });
|
var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { });
|
||||||
|
|
||||||
// Act 3
|
// Act 3
|
||||||
// Trigger change
|
// Trigger change
|
||||||
|
|
@ -146,11 +146,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
||||||
defaults: new { zipCode = 3510 },
|
defaults: new { zipCode = 3510 },
|
||||||
requiredValues: new { id = 7 });
|
metadataRequiredValues: new { id = 7 });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
||||||
defaults: new { id = 12 },
|
defaults: new { id = 12 },
|
||||||
requiredValues: new { zipCode = 3510 });
|
metadataRequiredValues: new { zipCode = 3510 });
|
||||||
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
|
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
||||||
defaults: new { zipCode = 3510 },
|
defaults: new { zipCode = 3510 },
|
||||||
requiredValues: new { id = 7 });
|
metadataRequiredValues: new { id = 7 });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
||||||
defaults: new { id = 12 });
|
defaults: new { id = 12 });
|
||||||
|
|
@ -198,15 +198,15 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
||||||
defaults: new { zipCode = 3510 },
|
defaults: new { zipCode = 3510 },
|
||||||
requiredValues: new { id = 7 });
|
metadataRequiredValues: new { id = 7 });
|
||||||
var endpoint2 = CreateEndpoint(
|
var endpoint2 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent}/{zipCode}",
|
||||||
defaults: new { id = 12 },
|
defaults: new { id = 12 },
|
||||||
requiredValues: new { id = 12 });
|
metadataRequiredValues: new { id = 12 });
|
||||||
var endpoint3 = CreateEndpoint(
|
var endpoint3 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
||||||
defaults: new { id = 12 },
|
defaults: new { id = 12 },
|
||||||
requiredValues: new { id = 12 });
|
metadataRequiredValues: new { id = 12 });
|
||||||
var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
|
var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -230,7 +230,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = CreateEndpoint(
|
var endpoint1 = CreateEndpoint(
|
||||||
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
|
||||||
defaults: new { zipCode = 3510 },
|
defaults: new { zipCode = 3510 },
|
||||||
requiredValues: new { id = 7 });
|
metadataRequiredValues: new { id = 7 });
|
||||||
var endpoint2 = CreateEndpoint("test");
|
var endpoint2 = CreateEndpoint("test");
|
||||||
|
|
||||||
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
|
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
|
||||||
|
|
@ -255,7 +255,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var expected = CreateEndpoint(
|
var expected = CreateEndpoint(
|
||||||
"api/orders/{id}",
|
"api/orders/{id}",
|
||||||
defaults: new { controller = "Orders", action = "GetById" },
|
defaults: new { controller = "Orders", action = "GetById" },
|
||||||
requiredValues: new { controller = "Orders", action = "GetById" },
|
metadataRequiredValues: new { controller = "Orders", action = "GetById" },
|
||||||
routeName: "OrdersApi");
|
routeName: "OrdersApi");
|
||||||
var addressScheme = CreateAddressScheme(expected);
|
var addressScheme = CreateAddressScheme(expected);
|
||||||
|
|
||||||
|
|
@ -273,6 +273,29 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
Assert.Same(expected, actual);
|
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]
|
[Fact]
|
||||||
public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues()
|
public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues()
|
||||||
{
|
{
|
||||||
|
|
@ -284,7 +307,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var expected = CreateEndpoint(
|
var expected = CreateEndpoint(
|
||||||
"api/orders/{id}",
|
"api/orders/{id}",
|
||||||
defaults: new { controller = "Orders", action = "GetById" },
|
defaults: new { controller = "Orders", action = "GetById" },
|
||||||
requiredValues: new { controller = "Orders", action = "GetById" },
|
metadataRequiredValues: new { controller = "Orders", action = "GetById" },
|
||||||
routeName: "OrdersApi");
|
routeName: "OrdersApi");
|
||||||
var addressScheme = CreateAddressScheme(expected);
|
var addressScheme = CreateAddressScheme(expected);
|
||||||
|
|
||||||
|
|
@ -345,7 +368,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
private RouteEndpoint CreateEndpoint(
|
private RouteEndpoint CreateEndpoint(
|
||||||
string template,
|
string template,
|
||||||
object defaults = null,
|
object defaults = null,
|
||||||
object requiredValues = null,
|
object metadataRequiredValues = null,
|
||||||
|
object routePatternRequiredValues = null,
|
||||||
int order = 0,
|
int order = 0,
|
||||||
string routeName = null,
|
string routeName = null,
|
||||||
EndpointMetadataCollection metadataCollection = null)
|
EndpointMetadataCollection metadataCollection = null)
|
||||||
|
|
@ -353,16 +377,16 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
if (metadataCollection == null)
|
if (metadataCollection == null)
|
||||||
{
|
{
|
||||||
var metadata = new List<object>();
|
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);
|
metadataCollection = new EndpointMetadataCollection(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RouteEndpoint(
|
return new RouteEndpoint(
|
||||||
TestConstants.EmptyRequestDelegate,
|
TestConstants.EmptyRequestDelegate,
|
||||||
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
|
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues),
|
||||||
order,
|
order,
|
||||||
metadataCollection,
|
metadataCollection,
|
||||||
null);
|
null);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue