From e2de54a92d8254a27f9eefd77e08370c7b17fa5d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 23 Aug 2018 21:42:42 +1200 Subject: [PATCH] Update MvcEndpointDataSource to use RoutePattern (#8249) --- .../MvcEndpointDatasourceBenchmark.cs | 12 +- build/dependencies.props | 4 +- .../MvcApplicationBuilderExtensions.cs | 6 +- .../Builder/MvcEndpointInfo.cs | 83 ++++------- .../Internal/MvcEndpointDataSource.cs | 137 ++++++++++++------ .../Internal/RoutePatternWriter.cs | 75 ++++++++++ .../Internal/RouteTemplateWriter.cs | 56 ------- .../Routing/ActionConstraintMatcherPolicy.cs | 4 +- .../MvcApplicationBuilderExtensionsTest.cs | 2 +- .../Internal/MvcEndpointDataSourceTests.cs | 23 +-- ...terTests.cs => RoutePatternWriterTests.cs} | 12 +- .../ActionConstraintMatcherPolicyTest.cs | 36 ++--- 12 files changed, 248 insertions(+), 202 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/RouteTemplateWriter.cs rename test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/{RouteTemplateWriterTests.cs => RoutePatternWriterTests.cs} (73%) diff --git a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs index bb66f15d28..85bf14c1cf 100644 --- a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Mvc.Performance { @@ -49,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Performance new RouteValueDictionary(), new Dictionary(), 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(); } diff --git a/build/dependencies.props b/build/dependencies.props index 5e0c447c05..d6804c2325 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -48,8 +48,8 @@ 2.2.0-preview1-34967 2.2.0-preview1-34967 2.2.0-preview1-34967 - 2.2.0-a-preview2-matcherendpoint-rename-16892 - 2.2.0-a-preview2-matcherendpoint-rename-16892 + 2.2.0-a-preview2-routepattern-defaults-16901 + 2.2.0-a-preview2-routepattern-defaults-16901 2.2.0-preview1-34967 2.2.0-preview1-34967 2.2.0-preview1-34967 diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs index a3d8fd03b0..06e26ed407 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs @@ -93,8 +93,8 @@ namespace Microsoft.AspNetCore.Builder .GetRequiredService>() .OfType() .First(); - var constraintResolver = app.ApplicationServices - .GetRequiredService(); + var parameterPolicyFactory = app.ApplicationServices + .GetRequiredService(); var endpointRouteBuilder = new EndpointRouteBuilder(app); @@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Builder route.Defaults, route.Constraints.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value), route.DataTokens, - constraintResolver); + parameterPolicyFactory); mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo); } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs index e0943b82c0..8b1eba5fbf 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Builder { @@ -13,35 +13,36 @@ namespace Microsoft.AspNetCore.Builder { public MvcEndpointInfo( string name, - string template, + string pattern, RouteValueDictionary defaults, IDictionary constraints, RouteValueDictionary dataTokens, - IInlineConstraintResolver constraintResolver) + ParameterPolicyFactory parameterPolicyFactory) { Name = name; - Template = template ?? string.Empty; + Pattern = pattern ?? string.Empty; DataTokens = dataTokens; 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. - ParsedTemplate = TemplateParser.Parse(template); + ParsedPattern = RoutePatternFactory.Parse(pattern, defaults, constraints); + Constraints = BuildConstraints(parameterPolicyFactory); - Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); 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) { 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 Template { get; } + public string Pattern { get; } // Non-inline defaults public RouteValueDictionary Defaults { get; } @@ -49,67 +50,33 @@ namespace Microsoft.AspNetCore.Builder // Inline and non-inline defaults merged into one public RouteValueDictionary MergedDefaults { get; } - public IDictionary Constraints { get; } + public IDictionary> Constraints { get; } public RouteValueDictionary DataTokens { get; } - internal RouteTemplate ParsedTemplate { get; private set; } + public RoutePattern ParsedPattern { get; private set; } - private static IDictionary GetConstraints( - IInlineConstraintResolver inlineConstraintResolver, - RouteTemplate parsedTemplate, - IDictionary constraints) + private Dictionary> BuildConstraints(ParameterPolicyFactory parameterPolicyFactory) { - var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText); + var constraints = new Dictionary>(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); - } - } - - 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)) + var createdPolicy = parameterPolicyFactory.Create(parameter, parameterPolicy); + if (createdPolicy is IRouteConstraint routeConstraint) { - if (!object.Equals(value, parameter.DefaultValue)) + if (!constraints.TryGetValue(parameter.Name, out var paramConstraints)) { - throw new InvalidOperationException( - 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)); + paramConstraints = new List(); + constraints.Add(parameter.Name, paramConstraints); } - } - else - { - result.Add(parameter.Name, parameter.DefaultValue); + + paramConstraints.Add(routeConstraint); } } } - return result; + return constraints; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index 2f40d07011..4150ed26f3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -12,7 +13,6 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.Internal @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal private readonly object _lock = new object(); private readonly IActionDescriptorCollectionProvider _actions; private readonly MvcEndpointInvokerFactory _invokerFactory; - private readonly IServiceProvider _serviceProvider; + private readonly DefaultHttpContext _httpContextInstance; private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders; private List _endpoints; @@ -55,8 +55,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal _actions = actions; _invokerFactory = invokerFactory; - _serviceProvider = serviceProvider; _actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray(); + _httpContextInstance = new DefaultHttpContext() { RequestServices = serviceProvider }; ConventionalEndpointInfos = new List(); @@ -67,7 +67,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal private List CreateEndpoints() { - List endpoints = new List(); + var endpoints = new List(); + StringBuilder patternStringBuilder = null; 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. var conventionalRouteOrder = 0; - // Check each of the conventional templates to see if the action would be reachable - // If the action and template are compatible then create an endpoint with the + // Check each of the conventional patterns to see if the action would be reachable + // If the action and pattern are compatible then create an endpoint with the // area/controller/action parameter parts replaced with literals // // e.g. {controller}/{action} with HomeController.Index and HomeController.Login @@ -91,32 +92,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal // - Home/Login foreach (var endpointInfo in ConventionalEndpointInfos) { - var actionRouteValues = action.RouteValues; - var endpointTemplateSegments = endpointInfo.ParsedTemplate.Segments; - if (MatchRouteValue(action, endpointInfo, "Area") && MatchRouteValue(action, endpointInfo, "Controller") && 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: // - /Home/Index/{id?} // - /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( action, endpointInfo.Name, - subTemplate, + GetPattern(ref patternStringBuilder, subPathSegments), + subPathSegments, endpointInfo.Defaults, ++conventionalRouteOrder, endpointInfo, @@ -124,25 +123,36 @@ namespace Microsoft.AspNetCore.Mvc.Internal endpoints.Add(subEndpoint); } - var segment = newEndpointTemplate.Segments[i]; + List segmentParts = null; // Initialize only as needed + var segment = newPathSegments[i]; for (var j = 0; j < segment.Parts.Count; 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 - 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( action, endpointInfo.Name, - newTemplate, + GetPattern(ref patternStringBuilder, newPathSegments), + newPathSegments, endpointInfo.Defaults, ++conventionalRouteOrder, endpointInfo, @@ -157,6 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal action, action.AttributeRouteInfo.Name, action.AttributeRouteInfo.Template, + RoutePatternFactory.Parse(action.AttributeRouteInfo.Template).PathSegments, nonInlineDefaults: null, action.AttributeRouteInfo.Order, action.AttributeRouteInfo, @@ -166,6 +177,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal } return endpoints; + + string GetPattern(ref StringBuilder sb, IEnumerable 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) @@ -184,28 +209,29 @@ namespace Microsoft.AspNetCore.Mvc.Internal int segmentIndex, ActionDescriptor action, MvcEndpointInfo endpointInfo, - RouteTemplate template) + List pathSegments) { // Check whether the remaining segments are all optional and one or more of them is // for area/controller/action and has a default value 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++) { var part = segment.Parts[j]; - if (part.IsOptional || part.IsOptionalSeperator || part.IsCatchAll) + if (part.IsParameter && part is RoutePatternParameterPart parameterPart) { - continue; - } - if (part.IsParameter) - { - if (IsMvcParameter(part.Name)) + if (parameterPart.IsOptional || parameterPart.IsCatchAll) { - if (endpointInfo.MergedDefaults[part.Name] is string defaultValue - && action.RouteValues.TryGetValue(part.Name, out var routeValue) + continue; + } + + 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)) { 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 return false; @@ -227,8 +268,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal 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 - // Check that the template does not have a parameter for the routeKey - var matchingParameter = endpointInfo.ParsedTemplate.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase)); + // Check that the pattern does not have a parameter for the routeKey + var matchingParameter = endpointInfo.ParsedPattern.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase)); if (matchingParameter == null) { return true; @@ -241,18 +282,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal 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) { // Check that the value matches against constraints on that parameter // e.g. For {controller:regex((Home|Login))} the controller value must match the regex - // - // 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)) + if (endpointInfo.Constraints.TryGetValue(routeKey, out var constraints)) { - // Did not match constraint - return false; + foreach (var constraint in constraints) + { + if (!constraint.Match(_httpContextInstance, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest)) + { + // Did not match constraint + return false; + } + } } return true; @@ -265,7 +309,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal private RouteEndpoint CreateEndpoint( ActionDescriptor action, string routeName, - string template, + string patternRawText, + IEnumerable segments, object nonInlineDefaults, int order, object source, @@ -301,7 +346,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var endpoint = new RouteEndpoint( requestDelegate, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), + RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments), order, metadataCollection, action.DisplayName); @@ -377,13 +422,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Template: {controller}/{action}/{category}/{id?} // Defaults(in-line or non in-line): category=products // 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 // // Template: {controller=Home}/{action=Index}/{category=products}/{id?} // Defaults: controller=Home, action=Index, category=products // 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 private void EnsureRequiredValuesInDefaults(IDictionary requiredValues, RouteValueDictionary defaults) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs new file mode 100644 index 0000000000..5beeb6baa6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RoutePatternWriter.cs @@ -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 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(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RouteTemplateWriter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RouteTemplateWriter.cs deleted file mode 100644 index b17df21be2..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RouteTemplateWriter.cs +++ /dev/null @@ -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 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; - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionConstraintMatcherPolicy.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionConstraintMatcherPolicy.cs index 46d9adb91c..f858f4fe1e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionConstraintMatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionConstraintMatcherPolicy.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing // Run really late. 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. // @@ -47,6 +47,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing { ApplyActionConstraints(httpContext, candidateSet); } + + return Task.CompletedTask; } private void ApplyActionConstraints( diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs index d731a697f2..3bd2cfa1f6 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs @@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder var endpointInfo = Assert.Single(mvcEndpointDataSource.ConventionalEndpointInfos); Assert.Equal("default", endpointInfo.Name); - Assert.Equal("{controller=Home}/{action=Index}/{id?}", endpointInfo.Template); + Assert.Equal("{controller=Home}/{action=Index}/{id?}", endpointInfo.Pattern); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs index ca09424a52..55fd51e0b8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Moq; @@ -185,9 +186,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal [InlineData("{controller}/{action}/{*catchAll}", new[] { "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}.{ext?}", new[] { "TestController/TestAction.{ext?}" })] - //[InlineData("{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" })] - public void Endpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) + [InlineData("{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" })] + [InlineData("{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" })] + public void Endpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns) { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( @@ -199,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var endpoints = dataSource.Endpoints; // Assert - var inspectors = finalEndpointTemplates + var inspectors = finalEndpointPatterns .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) .ToArray(); @@ -694,12 +695,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal IDictionary constraints = null, RouteValueDictionary dataTokens = null) { - var routeOptions = new RouteOptions(); - var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); - routeOptionsSetup.Configure(routeOptions); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddRouting(); - var constraintResolver = new DefaultInlineConstraintResolver(Options.Create(routeOptions)); - return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, constraintResolver); + var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); + serviceCollection.Configure(routeOptionsSetup.Configure); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var parameterPolicyFactory = serviceProvider.GetRequiredService(); + return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, parameterPolicyFactory); } private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RouteTemplateWriterTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RoutePatternWriterTests.cs similarity index 73% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RouteTemplateWriterTests.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RoutePatternWriterTests.cs index 68fcb544ac..c403213b72 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RouteTemplateWriterTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RoutePatternWriterTests.cs @@ -1,13 +1,14 @@ // 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.Text; using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Patterns; using Xunit; namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal { - public class RouteTemplateWriterTests + public class RoutePatternWriterTests { [Theory] [InlineData(@"")] @@ -23,11 +24,12 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal [InlineData(@"{p1}.{p2}.{p3}")] 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()); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionConstraintMatcherPolicyTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionConstraintMatcherPolicyTest.cs index a423dfd2e3..e2f02e7f63 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionConstraintMatcherPolicyTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionConstraintMatcherPolicyTest.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing public class ActionConstraintMatcherPolicyTest { [Fact] - public void Apply_CanBeAmbiguous() + public async Task Apply_CanBeAmbiguous() { // Arrange var actions = new ActionDescriptor[] @@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var selector = CreateSelector(actions); // Act - selector.Apply(new DefaultHttpContext(), candidateSet); + await selector.ApplyAsync(new DefaultHttpContext(), new EndpointFeature(), candidateSet); // Assert Assert.True(candidateSet[0].IsValidCandidate); @@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } [Fact] - public void Apply_PrefersActionWithConstraints() + public async Task Apply_PrefersActionWithConstraints() { // Arrange var actionWithConstraints = new ActionDescriptor() @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert Assert.True(candidateSet[0].IsValidCandidate); @@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } [Fact] - public void Apply_ConstraintsRejectAll() + public async Task Apply_ConstraintsRejectAll() { // Arrange var action1 = new ActionDescriptor() @@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert Assert.False(candidateSet[0].IsValidCandidate); @@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } [Fact] - public void Apply_ConstraintsRejectAll_DifferentStages() + public async Task Apply_ConstraintsRejectAll_DifferentStages() { // Arrange var action1 = new ActionDescriptor() @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert 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. [Fact] - public void Apply_ConstraintsInOrder() + public async Task Apply_ConstraintsInOrder() { // Arrange var best = new ActionDescriptor() @@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert Assert.True(candidateSet[0].IsValidCandidate); @@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } [Fact] - public void Apply_SkipsOverInvalidEndpoints() + public async Task Apply_SkipsOverInvalidEndpoints() { // Arrange var best = new ActionDescriptor() @@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert Assert.False(candidateSet[0].IsValidCandidate); @@ -220,7 +220,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } [Fact] - public void Apply_IncludesNonMvcEndpoints() + public async Task Apply_IncludesNonMvcEndpoints() { // Arrange var action1 = new ActionDescriptor() @@ -246,7 +246,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert 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. [Fact] - public void Apply_ConstraintsInOrder_MultipleStages() + public async Task Apply_ConstraintsInOrder_MultipleStages() { // Arrange var best = new ActionDescriptor() @@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert Assert.True(candidateSet[0].IsValidCandidate); @@ -295,7 +295,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } [Fact] - public void Apply_Fallback_ToActionWithoutConstraints() + public async Task Apply_Fallback_ToActionWithoutConstraints() { // Arrange var nomatch1 = new ActionDescriptor() @@ -328,7 +328,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var httpContext = CreateHttpContext("POST"); // Act - selector.Apply(httpContext, candidateSet); + await selector.ApplyAsync(httpContext, new EndpointFeature(), candidateSet); // Assert Assert.True(candidateSet[0].IsValidCandidate);