Add required values to RoutePattern (#912)

This commit is contained in:
Ryan Nowak 2018-11-16 23:02:48 -08:00 committed by James Newton-King
parent b6a1de5676
commit 807d4c97e3
13 changed files with 874 additions and 50 deletions

View File

@ -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;
}

View File

@ -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)
{

View File

@ -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;
}
}
}
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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)
{

View File

@ -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;

View File

@ -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>();

View File

@ -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)
{

View File

@ -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));
}
}
}

View File

@ -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()
{

View File

@ -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);