Update MvcEndpointDataSource to use RoutePattern (#8249)

This commit is contained in:
James Newton-King 2018-08-23 21:42:42 +12:00 committed by GitHub
parent 03da30f3bf
commit e2de54a92d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 248 additions and 202 deletions

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Mvc.Performance namespace Microsoft.AspNetCore.Mvc.Performance
{ {
@ -49,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Performance
new RouteValueDictionary(), new RouteValueDictionary(),
new Dictionary<string, object>(), new Dictionary<string, object>(),
new RouteValueDictionary(), new RouteValueDictionary(),
new MockInlineConstraintResolver()) new MockParameterPolicyFactory())
}; };
} }
@ -134,9 +135,14 @@ namespace Microsoft.AspNetCore.Mvc.Performance
} }
} }
private class MockInlineConstraintResolver : IInlineConstraintResolver private class MockParameterPolicyFactory : ParameterPolicyFactory
{ {
public IRouteConstraint ResolveConstraint(string inlineConstraint) public override IParameterPolicy Create(RoutePatternParameterPart parameter, string inlineText)
{
throw new NotImplementedException();
}
public override IParameterPolicy Create(RoutePatternParameterPart parameter, IParameterPolicy parameterPolicy)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -48,8 +48,8 @@
<MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion> <MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>
<MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion> <MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>
<MicrosoftAspNetCoreResponseCachingPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreResponseCachingPackageVersion> <MicrosoftAspNetCoreResponseCachingPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreResponseCachingPackageVersion>
<MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>2.2.0-a-preview2-matcherendpoint-rename-16892</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion> <MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>2.2.0-a-preview2-routepattern-defaults-16901</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>
<MicrosoftAspNetCoreRoutingPackageVersion>2.2.0-a-preview2-matcherendpoint-rename-16892</MicrosoftAspNetCoreRoutingPackageVersion> <MicrosoftAspNetCoreRoutingPackageVersion>2.2.0-a-preview2-routepattern-defaults-16901</MicrosoftAspNetCoreRoutingPackageVersion>
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreServerIISIntegrationPackageVersion> <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreServerKestrelPackageVersion> <MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreSessionPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreSessionPackageVersion> <MicrosoftAspNetCoreSessionPackageVersion>2.2.0-preview1-34967</MicrosoftAspNetCoreSessionPackageVersion>

View File

@ -93,8 +93,8 @@ namespace Microsoft.AspNetCore.Builder
.GetRequiredService<IEnumerable<EndpointDataSource>>() .GetRequiredService<IEnumerable<EndpointDataSource>>()
.OfType<MvcEndpointDataSource>() .OfType<MvcEndpointDataSource>()
.First(); .First();
var constraintResolver = app.ApplicationServices var parameterPolicyFactory = app.ApplicationServices
.GetRequiredService<IInlineConstraintResolver>(); .GetRequiredService<ParameterPolicyFactory>();
var endpointRouteBuilder = new EndpointRouteBuilder(app); var endpointRouteBuilder = new EndpointRouteBuilder(app);
@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Builder
route.Defaults, route.Defaults,
route.Constraints.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value), route.Constraints.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value),
route.DataTokens, route.DataTokens,
constraintResolver); parameterPolicyFactory);
mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo); mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo);
} }

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder namespace Microsoft.AspNetCore.Builder
{ {
@ -13,35 +13,36 @@ namespace Microsoft.AspNetCore.Builder
{ {
public MvcEndpointInfo( public MvcEndpointInfo(
string name, string name,
string template, string pattern,
RouteValueDictionary defaults, RouteValueDictionary defaults,
IDictionary<string, object> constraints, IDictionary<string, object> constraints,
RouteValueDictionary dataTokens, RouteValueDictionary dataTokens,
IInlineConstraintResolver constraintResolver) ParameterPolicyFactory parameterPolicyFactory)
{ {
Name = name; Name = name;
Template = template ?? string.Empty; Pattern = pattern ?? string.Empty;
DataTokens = dataTokens; DataTokens = dataTokens;
try try
{ {
// Data we parse from the template will be used to fill in the rest of the constraints or // Data we parse from the pattern will be used to fill in the rest of the constraints or
// defaults. The parser will throw for invalid routes. // defaults. The parser will throw for invalid routes.
ParsedTemplate = TemplateParser.Parse(template); ParsedPattern = RoutePatternFactory.Parse(pattern, defaults, constraints);
Constraints = BuildConstraints(parameterPolicyFactory);
Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints);
Defaults = defaults; Defaults = defaults;
MergedDefaults = GetDefaults(ParsedTemplate, defaults); // Merge defaults outside of RoutePattern because the defaults will already have values from pattern
MergedDefaults = new RouteValueDictionary(ParsedPattern.Defaults);
} }
catch (Exception exception) catch (Exception exception)
{ {
throw new RouteCreationException( throw new RouteCreationException(
string.Format(CultureInfo.CurrentCulture, "An error occurred while creating the route with name '{0}' and template '{1}'.", name, template), exception); string.Format(CultureInfo.CurrentCulture, "An error occurred while creating the route with name '{0}' and pattern '{1}'.", name, pattern), exception);
} }
} }
public string Name { get; } public string Name { get; }
public string Template { get; } public string Pattern { get; }
// Non-inline defaults // Non-inline defaults
public RouteValueDictionary Defaults { get; } public RouteValueDictionary Defaults { get; }
@ -49,67 +50,33 @@ namespace Microsoft.AspNetCore.Builder
// Inline and non-inline defaults merged into one // Inline and non-inline defaults merged into one
public RouteValueDictionary MergedDefaults { get; } public RouteValueDictionary MergedDefaults { get; }
public IDictionary<string, IRouteConstraint> Constraints { get; } public IDictionary<string, IList<IRouteConstraint>> Constraints { get; }
public RouteValueDictionary DataTokens { get; } public RouteValueDictionary DataTokens { get; }
internal RouteTemplate ParsedTemplate { get; private set; } public RoutePattern ParsedPattern { get; private set; }
private static IDictionary<string, IRouteConstraint> GetConstraints( private Dictionary<string, IList<IRouteConstraint>> BuildConstraints(ParameterPolicyFactory parameterPolicyFactory)
IInlineConstraintResolver inlineConstraintResolver,
RouteTemplate parsedTemplate,
IDictionary<string, object> constraints)
{ {
var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText); var constraints = new Dictionary<string, IList<IRouteConstraint>>(StringComparer.OrdinalIgnoreCase);
if (constraints != null) foreach (var parameter in ParsedPattern.Parameters)
{ {
foreach (var kvp in constraints) foreach (var parameterPolicy in parameter.ParameterPolicies)
{ {
constraintBuilder.AddConstraint(kvp.Key, kvp.Value); var createdPolicy = parameterPolicyFactory.Create(parameter, parameterPolicy);
} if (createdPolicy is IRouteConstraint routeConstraint)
}
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
return constraintBuilder.Build();
}
private static RouteValueDictionary GetDefaults(
RouteTemplate parsedTemplate,
RouteValueDictionary defaults)
{
var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults);
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
if (result.TryGetValue(parameter.Name, out var value))
{ {
if (!object.Equals(value, parameter.DefaultValue)) if (!constraints.TryGetValue(parameter.Name, out var paramConstraints))
{ {
throw new InvalidOperationException( paramConstraints = new List<IRouteConstraint>();
string.Format(CultureInfo.CurrentCulture, "The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.", parameter.Name)); constraints.Add(parameter.Name, paramConstraints);
} }
}
else paramConstraints.Add(routeConstraint);
{
result.Add(parameter.Name, parameter.DefaultValue);
} }
} }
} }
return result; return constraints;
} }
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
@ -12,7 +13,6 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private readonly object _lock = new object(); private readonly object _lock = new object();
private readonly IActionDescriptorCollectionProvider _actions; private readonly IActionDescriptorCollectionProvider _actions;
private readonly MvcEndpointInvokerFactory _invokerFactory; private readonly MvcEndpointInvokerFactory _invokerFactory;
private readonly IServiceProvider _serviceProvider; private readonly DefaultHttpContext _httpContextInstance;
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders; private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private List<Endpoint> _endpoints; private List<Endpoint> _endpoints;
@ -55,8 +55,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_actions = actions; _actions = actions;
_invokerFactory = invokerFactory; _invokerFactory = invokerFactory;
_serviceProvider = serviceProvider;
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray(); _actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
_httpContextInstance = new DefaultHttpContext() { RequestServices = serviceProvider };
ConventionalEndpointInfos = new List<MvcEndpointInfo>(); ConventionalEndpointInfos = new List<MvcEndpointInfo>();
@ -67,7 +67,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private List<Endpoint> CreateEndpoints() private List<Endpoint> CreateEndpoints()
{ {
List<Endpoint> endpoints = new List<Endpoint>(); var endpoints = new List<Endpoint>();
StringBuilder patternStringBuilder = null;
foreach (var action in _actions.ActionDescriptors.Items) foreach (var action in _actions.ActionDescriptors.Items)
{ {
@ -81,8 +82,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world. // This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
var conventionalRouteOrder = 0; var conventionalRouteOrder = 0;
// Check each of the conventional templates to see if the action would be reachable // Check each of the conventional patterns to see if the action would be reachable
// If the action and template are compatible then create an endpoint with the // If the action and pattern are compatible then create an endpoint with the
// area/controller/action parameter parts replaced with literals // area/controller/action parameter parts replaced with literals
// //
// e.g. {controller}/{action} with HomeController.Index and HomeController.Login // e.g. {controller}/{action} with HomeController.Index and HomeController.Login
@ -91,32 +92,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// - Home/Login // - Home/Login
foreach (var endpointInfo in ConventionalEndpointInfos) foreach (var endpointInfo in ConventionalEndpointInfos)
{ {
var actionRouteValues = action.RouteValues;
var endpointTemplateSegments = endpointInfo.ParsedTemplate.Segments;
if (MatchRouteValue(action, endpointInfo, "Area") if (MatchRouteValue(action, endpointInfo, "Area")
&& MatchRouteValue(action, endpointInfo, "Controller") && MatchRouteValue(action, endpointInfo, "Controller")
&& MatchRouteValue(action, endpointInfo, "Action")) && MatchRouteValue(action, endpointInfo, "Action"))
{ {
var newEndpointTemplate = TemplateParser.Parse(endpointInfo.Template); var newPathSegments = endpointInfo.ParsedPattern.PathSegments.ToList();
for (var i = 0; i < newEndpointTemplate.Segments.Count; i++) for (var i = 0; i < newPathSegments.Count; i++)
{ {
// Check if the template can be shortened because the remaining parameters are optional // Check if the pattern can be shortened because the remaining parameters are optional
// //
// e.g. Matching template {controller=Home}/{action=Index}/{id?} against HomeController.Index // e.g. Matching pattern {controller=Home}/{action=Index}/{id?} against HomeController.Index
// can resolve to the following endpoints: // can resolve to the following endpoints:
// - /Home/Index/{id?} // - /Home/Index/{id?}
// - /Home // - /Home
// - / // - /
if (UseDefaultValuePlusRemainingSegementsOptional(i, action, endpointInfo, newEndpointTemplate)) if (UseDefaultValuePlusRemainingSegementsOptional(i, action, endpointInfo, newPathSegments))
{ {
var subTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments.Take(i)); var subPathSegments = newPathSegments.Take(i);
var subEndpoint = CreateEndpoint( var subEndpoint = CreateEndpoint(
action, action,
endpointInfo.Name, endpointInfo.Name,
subTemplate, GetPattern(ref patternStringBuilder, subPathSegments),
subPathSegments,
endpointInfo.Defaults, endpointInfo.Defaults,
++conventionalRouteOrder, ++conventionalRouteOrder,
endpointInfo, endpointInfo,
@ -124,25 +123,36 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpoints.Add(subEndpoint); endpoints.Add(subEndpoint);
} }
var segment = newEndpointTemplate.Segments[i]; List<RoutePatternPart> segmentParts = null; // Initialize only as needed
var segment = newPathSegments[i];
for (var j = 0; j < segment.Parts.Count; j++) for (var j = 0; j < segment.Parts.Count; j++)
{ {
var part = segment.Parts[j]; var part = segment.Parts[j];
if (part.IsParameter && IsMvcParameter(part.Name)) if (part.IsParameter && part is RoutePatternParameterPart parameterPart && IsMvcParameter(parameterPart.Name))
{ {
if (segmentParts == null)
{
segmentParts = segment.Parts.ToList();
}
// Replace parameter with literal value // Replace parameter with literal value
segment.Parts[j] = TemplatePart.CreateLiteral(action.RouteValues[part.Name]); segmentParts[j] = RoutePatternFactory.LiteralPart(action.RouteValues[parameterPart.Name]);
} }
} }
}
var newTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments); // A parameter part was replaced so replace segment with updated parts
if (segmentParts != null)
{
newPathSegments[i] = RoutePatternFactory.Segment(segmentParts);
}
}
var endpoint = CreateEndpoint( var endpoint = CreateEndpoint(
action, action,
endpointInfo.Name, endpointInfo.Name,
newTemplate, GetPattern(ref patternStringBuilder, newPathSegments),
newPathSegments,
endpointInfo.Defaults, endpointInfo.Defaults,
++conventionalRouteOrder, ++conventionalRouteOrder,
endpointInfo, endpointInfo,
@ -157,6 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
action, action,
action.AttributeRouteInfo.Name, action.AttributeRouteInfo.Name,
action.AttributeRouteInfo.Template, action.AttributeRouteInfo.Template,
RoutePatternFactory.Parse(action.AttributeRouteInfo.Template).PathSegments,
nonInlineDefaults: null, nonInlineDefaults: null,
action.AttributeRouteInfo.Order, action.AttributeRouteInfo.Order,
action.AttributeRouteInfo, action.AttributeRouteInfo,
@ -166,6 +177,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
return endpoints; return endpoints;
string GetPattern(ref StringBuilder sb, IEnumerable<RoutePatternPathSegment> segments)
{
if (sb == null)
{
sb = new StringBuilder();
}
RoutePatternWriter.WriteString(sb, segments);
var rawPattern = sb.ToString();
sb.Length = 0;
return rawPattern;
}
} }
private bool IsMvcParameter(string name) private bool IsMvcParameter(string name)
@ -184,28 +209,29 @@ namespace Microsoft.AspNetCore.Mvc.Internal
int segmentIndex, int segmentIndex,
ActionDescriptor action, ActionDescriptor action,
MvcEndpointInfo endpointInfo, MvcEndpointInfo endpointInfo,
RouteTemplate template) List<RoutePatternPathSegment> pathSegments)
{ {
// Check whether the remaining segments are all optional and one or more of them is // Check whether the remaining segments are all optional and one or more of them is
// for area/controller/action and has a default value // for area/controller/action and has a default value
var usedDefaultValue = false; var usedDefaultValue = false;
for (var i = segmentIndex; i < template.Segments.Count; i++) for (var i = segmentIndex; i < pathSegments.Count; i++)
{ {
var segment = template.Segments[i]; var segment = pathSegments[i];
for (var j = 0; j < segment.Parts.Count; j++) for (var j = 0; j < segment.Parts.Count; j++)
{ {
var part = segment.Parts[j]; var part = segment.Parts[j];
if (part.IsOptional || part.IsOptionalSeperator || part.IsCatchAll) if (part.IsParameter && part is RoutePatternParameterPart parameterPart)
{ {
continue; if (parameterPart.IsOptional || parameterPart.IsCatchAll)
}
if (part.IsParameter)
{
if (IsMvcParameter(part.Name))
{ {
if (endpointInfo.MergedDefaults[part.Name] is string defaultValue continue;
&& action.RouteValues.TryGetValue(part.Name, out var routeValue) }
if (IsMvcParameter(parameterPart.Name))
{
if (endpointInfo.MergedDefaults[parameterPart.Name] is string defaultValue
&& action.RouteValues.TryGetValue(parameterPart.Name, out var routeValue)
&& string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase)) && string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase))
{ {
usedDefaultValue = true; usedDefaultValue = true;
@ -213,6 +239,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
} }
} }
else if (part.IsSeparator && part is RoutePatternSeparatorPart separatorPart
&& separatorPart.Content == ".")
{
// Check if this pattern ends in an optional extension, e.g. ".{ext?}"
// Current literal must be "." and followed by a single optional parameter part
var nextPartIndex = j + 1;
if (nextPartIndex == segment.Parts.Count - 1
&& segment.Parts[nextPartIndex].IsParameter
&& segment.Parts[nextPartIndex] is RoutePatternParameterPart extensionParameterPart
&& extensionParameterPart.IsOptional)
{
continue;
}
}
// Stop because there is a non-optional/non-defaulted trailing value // Stop because there is a non-optional/non-defaulted trailing value
return false; return false;
@ -227,8 +268,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (!action.RouteValues.TryGetValue(routeKey, out var actionValue) || string.IsNullOrWhiteSpace(actionValue)) if (!action.RouteValues.TryGetValue(routeKey, out var actionValue) || string.IsNullOrWhiteSpace(actionValue))
{ {
// Action does not have a value for this routeKey, most likely because action is not in an area // Action does not have a value for this routeKey, most likely because action is not in an area
// Check that the template does not have a parameter for the routeKey // Check that the pattern does not have a parameter for the routeKey
var matchingParameter = endpointInfo.ParsedTemplate.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase)); var matchingParameter = endpointInfo.ParsedPattern.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase));
if (matchingParameter == null) if (matchingParameter == null)
{ {
return true; return true;
@ -241,18 +282,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return true; return true;
} }
var matchingParameter = endpointInfo.ParsedTemplate.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase)); var matchingParameter = endpointInfo.ParsedPattern.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase));
if (matchingParameter != null) if (matchingParameter != null)
{ {
// Check that the value matches against constraints on that parameter // Check that the value matches against constraints on that parameter
// e.g. For {controller:regex((Home|Login))} the controller value must match the regex // e.g. For {controller:regex((Home|Login))} the controller value must match the regex
// if (endpointInfo.Constraints.TryGetValue(routeKey, out var constraints))
// REVIEW: This is really ugly
if (endpointInfo.Constraints.TryGetValue(routeKey, out var constraint)
&& !constraint.Match(new DefaultHttpContext() { RequestServices = _serviceProvider }, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest))
{ {
// Did not match constraint foreach (var constraint in constraints)
return false; {
if (!constraint.Match(_httpContextInstance, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest))
{
// Did not match constraint
return false;
}
}
} }
return true; return true;
@ -265,7 +309,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private RouteEndpoint CreateEndpoint( private RouteEndpoint CreateEndpoint(
ActionDescriptor action, ActionDescriptor action,
string routeName, string routeName,
string template, string patternRawText,
IEnumerable<RoutePatternPathSegment> segments,
object nonInlineDefaults, object nonInlineDefaults,
int order, int order,
object source, object source,
@ -301,7 +346,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var endpoint = new RouteEndpoint( var endpoint = new RouteEndpoint(
requestDelegate, requestDelegate,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments),
order, order,
metadataCollection, metadataCollection,
action.DisplayName); action.DisplayName);
@ -377,13 +422,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Template: {controller}/{action}/{category}/{id?} // Template: {controller}/{action}/{category}/{id?}
// Defaults(in-line or non in-line): category=products // Defaults(in-line or non in-line): category=products
// Required values: controller=foo, action=bar // Required values: controller=foo, action=bar
// Final constructed template: foo/bar/{category}/{id?} // Final constructed pattern: foo/bar/{category}/{id?}
// Final defaults: controller=foo, action=bar, category=products // Final defaults: controller=foo, action=bar, category=products
// //
// Template: {controller=Home}/{action=Index}/{category=products}/{id?} // Template: {controller=Home}/{action=Index}/{category=products}/{id?}
// Defaults: controller=Home, action=Index, category=products // Defaults: controller=Home, action=Index, category=products
// Required values: controller=foo, action=bar // Required values: controller=foo, action=bar
// Final constructed template: foo/bar/{category}/{id?} // Final constructed pattern: foo/bar/{category}/{id?}
// Final defaults: controller=foo, action=bar, category=products // Final defaults: controller=foo, action=bar, category=products
private void EnsureRequiredValuesInDefaults(IDictionary<string, string> requiredValues, RouteValueDictionary defaults) private void EnsureRequiredValuesInDefaults(IDictionary<string, string> requiredValues, RouteValueDictionary defaults)
{ {

View File

@ -0,0 +1,75 @@
// 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;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Mvc.Internal
{
internal static class RoutePatternWriter
{
public static void WriteString(StringBuilder sb, IEnumerable<RoutePatternPathSegment> routeSegments)
{
foreach (var segment in routeSegments)
{
if (sb.Length > 0)
{
sb.Append("/");
}
WriteString(sb, segment);
}
}
private static void WriteString(StringBuilder sb, RoutePatternPathSegment segment)
{
for (int i = 0; i < segment.Parts.Count; i++)
{
WriteString(sb, segment.Parts[i]);
}
}
private static void WriteString(StringBuilder sb, RoutePatternPart part)
{
if (part.IsParameter && part is RoutePatternParameterPart parameterPart)
{
sb.Append("{");
if (parameterPart.IsCatchAll)
{
sb.Append("*");
}
sb.Append(parameterPart.Name);
foreach (var item in parameterPart.ParameterPolicies)
{
sb.Append(":");
sb.Append(item.Content);
}
if (parameterPart.Default != null)
{
sb.Append("=");
sb.Append(parameterPart.Default);
}
if (parameterPart.IsOptional)
{
sb.Append("?");
}
sb.Append("}");
}
else if (part is RoutePatternLiteralPart literalPart)
{
sb.Append(literalPart.Content);
}
else if (part is RoutePatternSeparatorPart separatorPart)
{
sb.Append(separatorPart.Content);
}
else
{
throw new NotSupportedException();
}
}
}
}

View File

@ -1,56 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Mvc.Internal
{
internal static class RouteTemplateWriter
{
public static string ToString(IEnumerable<TemplateSegment> routeSegments)
{
return string.Join("/", routeSegments.Select(s => ToString(s)));
}
private static string ToString(TemplateSegment templateSegment)
{
return string.Join(string.Empty, templateSegment.Parts.Select(p => ToString(p)));
}
private static string ToString(TemplatePart templatePart)
{
if (templatePart.IsParameter)
{
var partText = "{";
if (templatePart.IsCatchAll)
{
partText += "*";
}
partText += templatePart.Name;
foreach (var item in templatePart.InlineConstraints)
{
partText += ":";
partText += item.Constraint;
}
if (templatePart.DefaultValue != null)
{
partText += "=";
partText += templatePart.DefaultValue;
}
if (templatePart.IsOptional)
{
partText += "?";
}
partText += "}";
return partText;
}
else
{
return templatePart.Text;
}
}
}
}

View File

@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// Run really late. // Run really late.
public override int Order => 100000; public override int Order => 100000;
public void Apply(HttpContext httpContext, CandidateSet candidateSet) public Task ApplyAsync(HttpContext httpContext, EndpointFeature endpointFeature, CandidateSet candidateSet)
{ {
// PERF: we can skip over action constraints if there aren't any app-wide. // PERF: we can skip over action constraints if there aren't any app-wide.
// //
@ -47,6 +47,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{ {
ApplyActionConstraints(httpContext, candidateSet); ApplyActionConstraints(httpContext, candidateSet);
} }
return Task.CompletedTask;
} }
private void ApplyActionConstraints( private void ApplyActionConstraints(

View File

@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder
var endpointInfo = Assert.Single(mvcEndpointDataSource.ConventionalEndpointInfos); var endpointInfo = Assert.Single(mvcEndpointDataSource.ConventionalEndpointInfos);
Assert.Equal("default", endpointInfo.Name); Assert.Equal("default", endpointInfo.Name);
Assert.Equal("{controller=Home}/{action=Index}/{id?}", endpointInfo.Template); Assert.Equal("{controller=Home}/{action=Index}/{id?}", endpointInfo.Pattern);
} }
} }
} }

View File

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Moq; using Moq;
@ -185,9 +186,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
[InlineData("{controller}/{action}/{*catchAll}", new[] { "TestController/TestAction/{*catchAll}" })] [InlineData("{controller}/{action}/{*catchAll}", new[] { "TestController/TestAction/{*catchAll}" })]
[InlineData("{controller}/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" })] [InlineData("{controller}/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" })]
[InlineData("{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" })] [InlineData("{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" })]
//[InlineData("{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" })] [InlineData("{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" })]
//[InlineData("{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" })] [InlineData("{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" })]
public void Endpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) public void Endpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns)
{ {
// Arrange // Arrange
var actionDescriptorCollection = GetActionDescriptorCollection( var actionDescriptorCollection = GetActionDescriptorCollection(
@ -199,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var endpoints = dataSource.Endpoints; var endpoints = dataSource.Endpoints;
// Assert // Assert
var inspectors = finalEndpointTemplates var inspectors = finalEndpointPatterns
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText))) .Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText)))
.ToArray(); .ToArray();
@ -694,12 +695,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
IDictionary<string, object> constraints = null, IDictionary<string, object> constraints = null,
RouteValueDictionary dataTokens = null) RouteValueDictionary dataTokens = null)
{ {
var routeOptions = new RouteOptions(); var serviceCollection = new ServiceCollection();
var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); serviceCollection.AddRouting();
routeOptionsSetup.Configure(routeOptions);
var constraintResolver = new DefaultInlineConstraintResolver(Options.Create<RouteOptions>(routeOptions)); var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, constraintResolver); serviceCollection.Configure<RouteOptions>(routeOptionsSetup.Configure);
var serviceProvider = serviceCollection.BuildServiceProvider();
var parameterPolicyFactory = serviceProvider.GetRequiredService<ParameterPolicyFactory>();
return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, parameterPolicyFactory);
} }
private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues) private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues)

View File

@ -1,13 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Patterns;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
{ {
public class RouteTemplateWriterTests public class RoutePatternWriterTests
{ {
[Theory] [Theory]
[InlineData(@"")] [InlineData(@"")]
@ -23,11 +24,12 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
[InlineData(@"{p1}.{p2}.{p3}")] [InlineData(@"{p1}.{p2}.{p3}")]
public void ToString_TemplateRoundtrips(string template) public void ToString_TemplateRoundtrips(string template)
{ {
var routeTemplate = TemplateParser.Parse(template); var routePattern = RoutePatternFactory.Parse(template);
var output = RouteTemplateWriter.ToString(routeTemplate.Segments); var sb = new StringBuilder();
RoutePatternWriter.WriteString(sb, routePattern.PathSegments);
Assert.Equal(template, output); Assert.Equal(template, sb.ToString());
} }
} }
} }

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public class ActionConstraintMatcherPolicyTest public class ActionConstraintMatcherPolicyTest
{ {
[Fact] [Fact]
public void Apply_CanBeAmbiguous() public async Task Apply_CanBeAmbiguous()
{ {
// Arrange // Arrange
var actions = new ActionDescriptor[] var actions = new ActionDescriptor[]
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var selector = CreateSelector(actions); var selector = CreateSelector(actions);
// Act // Act
selector.Apply(new DefaultHttpContext(), candidateSet); await selector.ApplyAsync(new DefaultHttpContext(), new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.True(candidateSet[0].IsValidCandidate); Assert.True(candidateSet[0].IsValidCandidate);
@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
[Fact] [Fact]
public void Apply_PrefersActionWithConstraints() public async Task Apply_PrefersActionWithConstraints()
{ {
// Arrange // Arrange
var actionWithConstraints = new ActionDescriptor() var actionWithConstraints = new ActionDescriptor()
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.True(candidateSet[0].IsValidCandidate); Assert.True(candidateSet[0].IsValidCandidate);
@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
[Fact] [Fact]
public void Apply_ConstraintsRejectAll() public async Task Apply_ConstraintsRejectAll()
{ {
// Arrange // Arrange
var action1 = new ActionDescriptor() var action1 = new ActionDescriptor()
@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.False(candidateSet[0].IsValidCandidate); Assert.False(candidateSet[0].IsValidCandidate);
@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
[Fact] [Fact]
public void Apply_ConstraintsRejectAll_DifferentStages() public async Task Apply_ConstraintsRejectAll_DifferentStages()
{ {
// Arrange // Arrange
var action1 = new ActionDescriptor() var action1 = new ActionDescriptor()
@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.False(candidateSet[0].IsValidCandidate); Assert.False(candidateSet[0].IsValidCandidate);
@ -147,7 +147,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// Due to ordering of stages, the first action will be better. // Due to ordering of stages, the first action will be better.
[Fact] [Fact]
public void Apply_ConstraintsInOrder() public async Task Apply_ConstraintsInOrder()
{ {
// Arrange // Arrange
var best = new ActionDescriptor() var best = new ActionDescriptor()
@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.True(candidateSet[0].IsValidCandidate); Assert.True(candidateSet[0].IsValidCandidate);
@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
[Fact] [Fact]
public void Apply_SkipsOverInvalidEndpoints() public async Task Apply_SkipsOverInvalidEndpoints()
{ {
// Arrange // Arrange
var best = new ActionDescriptor() var best = new ActionDescriptor()
@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.False(candidateSet[0].IsValidCandidate); Assert.False(candidateSet[0].IsValidCandidate);
@ -220,7 +220,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
[Fact] [Fact]
public void Apply_IncludesNonMvcEndpoints() public async Task Apply_IncludesNonMvcEndpoints()
{ {
// Arrange // Arrange
var action1 = new ActionDescriptor() var action1 = new ActionDescriptor()
@ -246,7 +246,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.False(candidateSet[0].IsValidCandidate); Assert.False(candidateSet[0].IsValidCandidate);
@ -256,7 +256,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// Due to ordering of stages, the first action will be better. // Due to ordering of stages, the first action will be better.
[Fact] [Fact]
public void Apply_ConstraintsInOrder_MultipleStages() public async Task Apply_ConstraintsInOrder_MultipleStages()
{ {
// Arrange // Arrange
var best = new ActionDescriptor() var best = new ActionDescriptor()
@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.True(candidateSet[0].IsValidCandidate); Assert.True(candidateSet[0].IsValidCandidate);
@ -295,7 +295,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
[Fact] [Fact]
public void Apply_Fallback_ToActionWithoutConstraints() public async Task Apply_Fallback_ToActionWithoutConstraints()
{ {
// Arrange // Arrange
var nomatch1 = new ActionDescriptor() var nomatch1 = new ActionDescriptor()
@ -328,7 +328,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = CreateHttpContext("POST"); var httpContext = CreateHttpContext("POST");
// Act // Act
selector.Apply(httpContext, candidateSet); await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet);
// Assert // Assert
Assert.True(candidateSet[0].IsValidCandidate); Assert.True(candidateSet[0].IsValidCandidate);