Support RoutePattern required values in matcher and link generator

This commit is contained in:
James Newton-King 2018-11-27 17:42:10 +13:00
parent 142b6a73ec
commit 96cd055e05
31 changed files with 1145 additions and 110 deletions

View File

@ -116,11 +116,14 @@ namespace Microsoft.AspNetCore.Routing
params object[] metadata)
{
var endpointMetadata = new List<object>(metadata ?? Array.Empty<object>());
endpointMetadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues)));
if (routeName != null)
{
endpointMetadata.Add(new RouteNameMetadata(routeName));
}
return new RouteEndpoint(
(context) => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, constraints),
RoutePatternFactory.Parse(template, defaults, constraints, requiredValues),
order,
new EndpointMetadataCollection(endpointMetadata),
displayName);
@ -139,16 +142,13 @@ namespace Microsoft.AspNetCore.Routing
protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint)
{
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
var requiredValues = routeValuesAddressMetadata?.RequiredValues ?? new RouteValueDictionary();
treeRouteBuilder.MapOutbound(
NullRouter.Instance,
new RouteTemplate(RoutePatternFactory.Parse(
endpoint.RoutePattern.RawText,
defaults: endpoint.RoutePattern.Defaults,
parameterPolicies: null)),
requiredLinkValues: new RouteValueDictionary(requiredValues),
requiredLinkValues: new RouteValueDictionary(endpoint.RoutePattern.RequiredValues),
routeName: null,
order: 0);
}

View File

@ -0,0 +1,38 @@
// 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.Http;
using Microsoft.AspNetCore.Routing.Patterns;
namespace RoutingSandbox.Framework
{
public class FrameworkConfigurationBuilder
{
private readonly FrameworkEndpointDataSource _dataSource;
internal FrameworkConfigurationBuilder(FrameworkEndpointDataSource dataSource)
{
_dataSource = dataSource;
}
public void AddPattern(string pattern)
{
AddPattern(RoutePatternFactory.Parse(pattern));
}
public void AddPattern(RoutePattern pattern)
{
_dataSource.Patterns.Add(pattern);
}
public void AddHubMethod(string hub, string method, RequestDelegate requestDelegate)
{
_dataSource.HubMethods.Add(new HubMethod
{
Hub = hub,
Method = method,
RequestDelegate = requestDelegate
});
}
}
}

View File

@ -0,0 +1,100 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace RoutingSandbox.Framework
{
internal class FrameworkEndpointDataSource : EndpointDataSource, IEndpointConventionBuilder
{
private readonly RoutePatternTransformer _routePatternTransformer;
private readonly List<Action<EndpointModel>> _conventions;
public List<RoutePattern> Patterns { get; }
public List<HubMethod> HubMethods { get; }
private List<Endpoint> _endpoints;
public FrameworkEndpointDataSource(RoutePatternTransformer routePatternTransformer)
{
_routePatternTransformer = routePatternTransformer;
_conventions = new List<Action<EndpointModel>>();
Patterns = new List<RoutePattern>();
HubMethods = new List<HubMethod>();
}
public override IReadOnlyList<Endpoint> Endpoints
{
get
{
if (_endpoints == null)
{
_endpoints = BuildEndpoints();
}
return _endpoints;
}
}
private List<Endpoint> BuildEndpoints()
{
List<Endpoint> endpoints = new List<Endpoint>();
foreach (var hubMethod in HubMethods)
{
var requiredValues = new { hub = hubMethod.Hub, method = hubMethod.Method };
var order = 1;
foreach (var pattern in Patterns)
{
var resolvedPattern = _routePatternTransformer.SubstituteRequiredValues(pattern, requiredValues);
if (resolvedPattern == null)
{
continue;
}
var endpointModel = new RouteEndpointModel(
hubMethod.RequestDelegate,
resolvedPattern,
order++);
endpointModel.DisplayName = $"{hubMethod.Hub}.{hubMethod.Method}";
foreach (var convention in _conventions)
{
convention(endpointModel);
}
endpoints.Add(endpointModel.Build());
}
}
return endpoints;
}
public override IChangeToken GetChangeToken()
{
return NullChangeToken.Singleton;
}
public void Apply(Action<EndpointModel> convention)
{
_conventions.Add(convention);
}
}
internal class HubMethod
{
public string Hub { get; set; }
public string Method { get; set; }
public RequestDelegate RequestDelegate { get; set; }
}
}

View File

@ -0,0 +1,38 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
namespace RoutingSandbox.Framework
{
public static class FrameworkEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapFramework(this IEndpointRouteBuilder builder, Action<FrameworkConfigurationBuilder> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
var dataSource = builder.ServiceProvider.GetRequiredService<FrameworkEndpointDataSource>();
var configurationBuilder = new FrameworkConfigurationBuilder(dataSource);
configure(configurationBuilder);
builder.DataSources.Add(dataSource);
return dataSource;
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>

View File

@ -0,0 +1,18 @@
// 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.Text.RegularExpressions;
using Microsoft.AspNetCore.Routing;
namespace RoutingSandbox
{
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
// Slugify value
return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2", RegexOptions.None, TimeSpan.FromMilliseconds(100)).ToLower();
}
}
}

View File

@ -10,9 +10,8 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using RoutingSandbox.Framework;
namespace RoutingSandbox
{
@ -23,7 +22,11 @@ namespace RoutingSandbox
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
services.AddSingleton<FrameworkEndpointDataSource>();
}
public void Configure(IApplicationBuilder app)
@ -72,12 +75,22 @@ namespace RoutingSandbox
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
{
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
graphWriter.Write(dataSource, writer);
}
return Task.CompletedTask;
});
builder.MapFramework(frameworkBuilder =>
{
frameworkBuilder.AddPattern("/transform/{hub:slugify=TestHub}/{method:slugify=TestMethod}");
frameworkBuilder.AddPattern("/{hub}/{method=TestMethod}");
frameworkBuilder.AddHubMethod("TestHub", "TestMethod", context => context.Response.WriteAsync("TestMethod!"));
frameworkBuilder.AddHubMethod("Login", "Authenticate", context => context.Response.WriteAsync("Authenticate!"));
frameworkBuilder.AddHubMethod("Login", "Logout", context => context.Response.WriteAsync("Logout!"));
});
});
app.UseStaticFiles();

View File

@ -167,13 +167,14 @@ namespace Microsoft.AspNetCore.Routing
sb.Append(", Defaults: new { ");
sb.Append(string.Join(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)));
sb.Append(" }");
var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
var routeNameMetadata = routeEndpoint.Metadata.GetMetadata<IRouteNameMetadata>();
sb.Append(", Route Name: ");
sb.Append(routeValuesAddressMetadata?.RouteName);
if (routeValuesAddressMetadata?.RequiredValues != null)
sb.Append(routeNameMetadata?.RouteName);
var routeValues = routeEndpoint.RoutePattern.RequiredValues;
if (routeValues.Count > 0)
{
sb.Append(", Required Values: new { ");
sb.Append(string.Join(", ", FormatValues(routeValuesAddressMetadata.RequiredValues)));
sb.Append(string.Join(", ", FormatValues(routeValues)));
sb.Append(" }");
}
sb.Append(", Order: ");

View File

@ -311,7 +311,7 @@ namespace Microsoft.AspNetCore.Routing
_uriBuildingContextPool,
endpoint.RoutePattern,
new RouteValueDictionary(endpoint.RoutePattern.Defaults),
endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>()?.RequiredValues.Keys,
endpoint.RoutePattern.RequiredValues.Keys,
policies);
}

View File

@ -0,0 +1,17 @@
// 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
{
/// <summary>
/// Represents metadata used during link generation to find
/// the associated endpoint using route name.
/// </summary>
public interface IRouteNameMetadata
{
/// <summary>
/// Gets the route name. Can be null.
/// </summary>
string RouteName { get; }
}
}

View File

@ -1,6 +1,7 @@
// 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
@ -9,6 +10,7 @@ namespace Microsoft.AspNetCore.Routing
/// Represents metadata used during link generation to find
/// the associated endpoint using route values.
/// </summary>
[Obsolete("Route values are now specified on a RoutePattern.")]
public interface IRouteValuesAddressMetadata
{
/// <summary>

View File

@ -143,24 +143,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
var parent = parents[j];
var part = segment.Parts[0];
var parameterPart = part as RoutePatternParameterPart;
if (segment.IsSimple && part is RoutePatternLiteralPart literalPart)
{
DfaNode next = null;
var literal = literalPart.Content;
if (parent.Literals == null ||
!parent.Literals.TryGetValue(literal, out next))
{
next = new DfaNode()
{
PathDepth = parent.PathDepth + 1,
Label = includeLabel ? parent.Label + literal + "/" : null,
};
parent.AddLiteral(literal, next);
}
nextParents.Add(next);
AddLiteralNode(includeLabel, nextParents, parent, literalPart.Content);
}
else if (segment.IsSimple && part is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll)
else if (segment.IsSimple && parameterPart != null && parameterPart.IsCatchAll)
{
// A catch all should traverse all literal nodes as well as parameter nodes
// we don't need to create the parameter node here because of ordering
@ -194,7 +182,29 @@ namespace Microsoft.AspNetCore.Routing.Matching
parent.CatchAll.AddMatch(endpoint);
}
else if (segment.IsSimple && part.IsParameter)
else if (segment.IsSimple && parameterPart != null && TryGetRequiredValue(endpoint.RoutePattern, parameterPart, out var requiredValue))
{
// If the parameter has a matching required value, replace the parameter with the required value
// as a literal. This should use the parameter's transformer (if present)
// e.g. Template: Home/{action}, Required values: { action = "Index" }, Result: Home/Index
if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicyReferences))
{
for (var k = 0; k < parameterPolicyReferences.Count; k++)
{
var reference = parameterPolicyReferences[k];
var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference);
if (parameterPolicy is IOutboundParameterTransformer parameterTransformer)
{
requiredValue = parameterTransformer.TransformOutbound(requiredValue);
break;
}
}
}
AddLiteralNode(includeLabel, nextParents, parent, requiredValue.ToString());
}
else if (segment.IsSimple && parameterPart != null)
{
if (parent.Parameters == null)
{
@ -254,6 +264,23 @@ namespace Microsoft.AspNetCore.Routing.Matching
return root;
}
private static void AddLiteralNode(bool includeLabel, List<DfaNode> nextParents, DfaNode parent, string literal)
{
DfaNode next = null;
if (parent.Literals == null ||
!parent.Literals.TryGetValue(literal, out next))
{
next = new DfaNode()
{
PathDepth = parent.PathDepth + 1,
Label = includeLabel ? parent.Label + literal + "/" : null,
};
parent.AddLiteral(literal, next);
}
nextParents.Add(next);
}
private RoutePatternPathSegment GetCurrentSegment(RouteEndpoint endpoint, int depth)
{
if (depth < endpoint.RoutePattern.PathSegments.Count)
@ -500,11 +527,25 @@ namespace Microsoft.AspNetCore.Routing.Matching
slotIndex = _assignments.Count;
_assignments.Add(parameterPart.Name, slotIndex);
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
_slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
// A parameter can have a required value, default value/catch all, or be a normal parameter
// Add the required value or default value as the slot's initial value
if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out var requiredValue))
{
_slots.Add(new KeyValuePair<string, object>(parameterPart.Name, requiredValue));
}
else
{
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
_slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
}
}
if (parameterPart.IsCatchAll)
if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out _))
{
// Don't capture a parameter if it has a required value
// There is no need because a parameter with a required value is matched as a literal
}
else if (parameterPart.IsCatchAll)
{
catchAll = (parameterPart.Name, i, slotIndex);
}
@ -720,5 +761,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
return (nodeBuilderPolicies.ToArray(), endpointComparerPolicies.ToArray(), endpointSelectorPolicies.ToArray());
}
private static bool TryGetRequiredValue(RoutePattern routePattern, RoutePatternParameterPart parameterPart, out object value)
{
if (!routePattern.RequiredValues.TryGetValue(parameterPart.Name, out value))
{
return false;
}
return !RouteValueEqualityComparer.Default.Equals(value, string.Empty);
}
}
}

View File

@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
updatedDefaults ?? original.Defaults,
original.ParameterPolicies,
requiredValues,
updatedParameters ?? original.Parameters,
updatedParameters ?? original.Parameters,
updatedSegments ?? original.PathSegments);
}

View File

@ -0,0 +1,34 @@
// 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.Diagnostics;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Metadata used during link generation to find the associated endpoint using route name.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
public sealed class RouteNameMetadata : IRouteNameMetadata
{
/// <summary>
/// Creates a new instance of <see cref="RouteNameMetadata"/> with the provided route name.
/// </summary>
/// <param name="routeName">The route name. Can be null.</param>
public RouteNameMetadata(string routeName)
{
RouteName = routeName;
}
/// <summary>
/// Gets the route name. Can be null.
/// </summary>
public string RouteName { get; }
internal string DebuggerToString()
{
return $"Name: {RouteName}";
}
}
}

View File

@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Routing
/// Metadata used during link generation to find the associated endpoint using route values.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[Obsolete("Route values are now specified on a RoutePattern.")]
public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata
{
private static readonly IReadOnlyDictionary<string, object> EmptyRouteValues =

View File

@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Routing
continue;
}
var metadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
var metadata = endpoint.Metadata.GetMetadata<IRouteNameMetadata>();
if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0)
{
continue;
@ -137,8 +137,8 @@ namespace Microsoft.AspNetCore.Routing
}
var entry = CreateOutboundRouteEntry(
routeEndpoint,
metadata?.RequiredValues ?? routeEndpoint.RoutePattern.RequiredValues,
routeEndpoint,
routeEndpoint.RoutePattern.RequiredValues,
metadata?.RouteName);
var outboundMatch = new OutboundMatch() { Entry = entry };

View File

@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Routing.Template
{
var segment = routePattern.PathSegments[i];
var digit = ComputeInboundPrecedenceDigit(segment);
var digit = ComputeInboundPrecedenceDigit(routePattern, segment);
Debug.Assert(digit >= 0 && digit < 10);
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
@ -217,7 +217,9 @@ namespace Microsoft.AspNetCore.Routing.Template
}
// see description on ComputeInboundPrecedenceDigit(TemplateSegment segment)
private static int ComputeInboundPrecedenceDigit(RoutePatternPathSegment pathSegment)
//
// With a RoutePattern, parameters with a required value are treated as a literal segment
private static int ComputeInboundPrecedenceDigit(RoutePattern routePattern, RoutePatternPathSegment pathSegment)
{
if (pathSegment.Parts.Count > 1)
{
@ -233,6 +235,13 @@ namespace Microsoft.AspNetCore.Routing.Template
}
else if (part is RoutePatternParameterPart parameterPart)
{
// Parameter with a required value is matched as a literal
if (routePattern.RequiredValues.TryGetValue(parameterPart.Name, out var requiredValue) &&
!RouteValueEqualityComparer.Default.Equals(requiredValue, string.Empty))
{
return 1;
}
var digit = parameterPart.IsCatchAll ? 5 : 3;
// If there is a route constraint for the parameter, reduce order by 1

View File

@ -174,11 +174,23 @@ namespace Microsoft.AspNetCore.Routing.Template
ambientValueProcessedCount++;
}
if (hasExplicitValue)
// For now, only check ambient values with required values that don't have a parameter
// Ambient values for parameters are processed below
var hasParameter = _pattern.GetParameter(key) != null;
if (!hasParameter)
{
// Note that we don't increment valueProcessedCount here. We expect required values
// to also be filters, which are tracked when we populate 'slots'.
if (!RoutePartsEqual(value, ambientValue))
if (!_pattern.RequiredValues.TryGetValue(key, out var requiredValue))
{
throw new InvalidOperationException($"Unable to find required value '{key}' on route pattern.");
}
if (!RoutePartsEqual(ambientValue, _pattern.RequiredValues[key]))
{
copyAmbientValues = false;
break;
}
if (hasExplicitValue && !RoutePartsEqual(value, ambientValue))
{
copyAmbientValues = false;
break;

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Moq;

View File

@ -309,14 +309,6 @@ namespace Microsoft.AspNetCore.Routing
Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path);
}
private class UpperCaseParameterTransform : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
return value?.ToString()?.ToUpperInvariant();
}
}
[Fact]
public void GetLink_ParameterTransformer()
{
@ -346,7 +338,7 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint(
"{controller:upper-case}/{name}",
requiredValues: new { controller = "Home", name = "Test", c = "hithere", },
requiredValues: new { controller = "Home", name = "Test", },
policies: new { c = new UpperCaseParameterTransform(), });
Action<IServiceCollection> configure = (s) =>
@ -586,24 +578,24 @@ namespace Microsoft.AspNetCore.Routing
"Home/Index",
order: 3,
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpointController = EndpointFactory.CreateRouteEndpoint(
"Home",
order: 2,
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpointEmpty = EndpointFactory.CreateRouteEndpoint(
"",
order: 1,
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
// This endpoint should be used to generate the link when an id is present
var endpointControllerActionParameter = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id}",
order: 0,
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var linkGenerator = CreateLinkGenerator(endpointControllerAction, endpointController, endpointEmpty, endpointControllerActionParameter);
@ -642,11 +634,11 @@ namespace Microsoft.AspNetCore.Routing
var homeIndex = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var homeLogin = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Login", })) });
requiredValues: new { controller = "Home", action = "Login", });
var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin);
@ -685,11 +677,11 @@ namespace Microsoft.AspNetCore.Routing
var homeIndex = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var homeLogin = EndpointFactory.CreateRouteEndpoint(
"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Login", })) });
requiredValues: new { controller = "Home", action = "Login", });
var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin);

View File

@ -3,9 +3,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing
{
@ -20,17 +24,22 @@ namespace Microsoft.AspNetCore.Routing
string displayName = null,
params object[] metadata)
{
var d = new List<object>(metadata ?? Array.Empty<object>());
if (requiredValues != null)
{
d.Add(new RouteValuesAddressMetadata(new RouteValueDictionary(requiredValues)));
}
var routePattern = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
return CreateRouteEndpoint(routePattern, order, displayName, metadata);
}
public static RouteEndpoint CreateRouteEndpoint(
RoutePattern routePattern = null,
int order = 0,
string displayName = null,
IList<object> metadata = null)
{
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse(template, defaults, policies),
routePattern,
order,
new EndpointMetadataCollection(d),
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
displayName);
}
}

View File

@ -24,11 +24,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
@ -86,11 +86,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
@ -116,11 +116,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
@ -145,11 +145,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
@ -177,11 +177,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
"Home/Index/{id?}",
defaults: new { controller = "Home", action = "Index", },
metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) });
requiredValues: new { controller = "Home", action = "Index", });
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);

View File

@ -7,6 +7,8 @@ using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
@ -727,6 +729,253 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.Null(a.PolicyEdges);
}
[Fact]
public void BuildDfaTree_RequiredValues()
{
// Arrange
var builder = CreateDfaMatcherBuilder();
var endpoint = CreateEndpoint("{controller}/{action}", requiredValues: new { controller = "Home", action = "Index" });
builder.AddEndpoint(endpoint);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("Home", next.Key);
var home = next.Value;
Assert.Null(home.Matches);
Assert.Null(home.Parameters);
next = Assert.Single(home.Literals);
Assert.Equal("Index", next.Key);
var index = next.Value;
Assert.Same(endpoint, Assert.Single(index.Matches));
Assert.Null(index.Literals);
}
[Fact]
public void BuildDfaTree_RequiredValues_AndMatchingDefaults()
{
// Arrange
var builder = CreateDfaMatcherBuilder();
var endpoint = CreateEndpoint(
"{controller}/{action}",
defaults: new { controller = "Home", action = "Index" },
requiredValues: new { controller = "Home", action = "Index" });
builder.AddEndpoint(endpoint);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Same(endpoint, Assert.Single(root.Matches));
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("Home", next.Key);
var home = next.Value;
Assert.Same(endpoint, Assert.Single(home.Matches));
Assert.Null(home.Parameters);
next = Assert.Single(home.Literals);
Assert.Equal("Index", next.Key);
var index = next.Value;
Assert.Same(endpoint, Assert.Single(index.Matches));
Assert.Null(index.Literals);
}
[Fact]
public void BuildDfaTree_RequiredValues_AndDifferentDefaults()
{
// Arrange
var builder = CreateDfaMatcherBuilder();
var endpoint = CreateSubsitutedEndpoint(
"{controller}/{action}",
defaults: new { controller = "Home", action = "Index" },
requiredValues: new { controller = "Login", action = "Index" });
builder.AddEndpoint(endpoint);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("Login", next.Key);
var login = next.Value;
Assert.Same(endpoint, Assert.Single(login.Matches));
Assert.Null(login.Parameters);
next = Assert.Single(login.Literals);
Assert.Equal("Index", next.Key);
var index = next.Value;
Assert.Same(endpoint, Assert.Single(index.Matches));
Assert.Null(index.Literals);
}
[Fact]
public void BuildDfaTree_RequiredValues_Multiple()
{
// Arrange
var builder = CreateDfaMatcherBuilder();
var endpoint1 = CreateSubsitutedEndpoint(
"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" },
requiredValues: new { controller = "Home", action = "Index" });
builder.AddEndpoint(endpoint1);
var endpoint2 = CreateSubsitutedEndpoint(
"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" },
requiredValues: new { controller = "Login", action = "Index" });
builder.AddEndpoint(endpoint2);
var endpoint3 = CreateSubsitutedEndpoint(
"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" },
requiredValues: new { controller = "Login", action = "ChangePassword" });
builder.AddEndpoint(endpoint3);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Same(endpoint1, Assert.Single(root.Matches));
Assert.Null(root.Parameters);
Assert.Equal(2, root.Literals.Count);
var home = root.Literals["Home"];
Assert.Same(endpoint1, Assert.Single(home.Matches));
Assert.Null(home.Parameters);
var next = Assert.Single(home.Literals);
Assert.Equal("Index", next.Key);
var homeIndex = next.Value;
Assert.Same(endpoint1, Assert.Single(homeIndex.Matches));
Assert.Null(homeIndex.Literals);
Assert.NotNull(homeIndex.Parameters);
Assert.Same(endpoint1, Assert.Single(homeIndex.Parameters.Matches));
var login = root.Literals["Login"];
Assert.Same(endpoint2, Assert.Single(login.Matches));
Assert.Null(login.Parameters);
Assert.Equal(2, login.Literals.Count);
var loginIndex = login.Literals["Index"];
Assert.Same(endpoint2, Assert.Single(loginIndex.Matches));
Assert.Null(loginIndex.Literals);
Assert.NotNull(loginIndex.Parameters);
Assert.Same(endpoint2, Assert.Single(loginIndex.Parameters.Matches));
var loginChangePassword = login.Literals["ChangePassword"];
Assert.Same(endpoint3, Assert.Single(loginChangePassword.Matches));
Assert.Null(loginChangePassword.Literals);
Assert.NotNull(loginChangePassword.Parameters);
Assert.Same(endpoint3, Assert.Single(loginChangePassword.Parameters.Matches));
}
[Fact]
public void BuildDfaTree_RequiredValues_AndParameterTransformer()
{
// Arrange
var builder = CreateDfaMatcherBuilder();
var endpoint = CreateEndpoint(
"{controller:slugify}/{action:slugify}",
defaults: new { controller = "RecentProducts", action = "ViewAll" },
requiredValues: new { controller = "RecentProducts", action = "ViewAll" });
builder.AddEndpoint(endpoint);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Same(endpoint, Assert.Single(root.Matches));
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("recent-products", next.Key);
var home = next.Value;
Assert.Same(endpoint, Assert.Single(home.Matches));
Assert.Null(home.Parameters);
next = Assert.Single(home.Literals);
Assert.Equal("view-all", next.Key);
var index = next.Value;
Assert.Same(endpoint, Assert.Single(index.Matches));
Assert.Null(index.Literals);
}
[Fact]
public void BuildDfaTree_RequiredValues_AndDefaults_AndParameterTransformer()
{
// Arrange
var builder = CreateDfaMatcherBuilder();
var endpoint = CreateEndpoint(
"ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}",
requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null });
builder.AddEndpoint(endpoint);
// Act
var root = builder.BuildDfaTree();
// Assert
Assert.Null(root.Matches);
Assert.Null(root.Parameters);
var next = Assert.Single(root.Literals);
Assert.Equal("ConventionalTransformerRoute", next.Key);
var conventionalTransformerRoute = next.Value;
Assert.Null(conventionalTransformerRoute.Matches);
Assert.Null(conventionalTransformerRoute.Parameters);
next = Assert.Single(conventionalTransformerRoute.Literals);
Assert.Equal("conventional-transformer", next.Key);
var conventionalTransformer = next.Value;
Assert.Same(endpoint, Assert.Single(conventionalTransformer.Matches));
next = Assert.Single(conventionalTransformer.Literals);
Assert.Equal("Index", next.Key);
var index = next.Value;
Assert.Same(endpoint, Assert.Single(index.Matches));
Assert.NotNull(index.Parameters);
Assert.Same(endpoint, Assert.Single(index.Parameters.Matches));
}
[Fact]
public void CreateCandidate_JustLiterals()
{
@ -971,26 +1220,70 @@ namespace Microsoft.AspNetCore.Routing.Matching
private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies)
{
var policyFactory = CreateParameterPolicyFactory();
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
return new DfaMatcherBuilder(
NullLoggerFactory.Instance,
new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), Mock.Of<IServiceProvider>()),
policyFactory,
Mock.Of<EndpointSelector>(),
policies);
}
private RouteEndpoint CreateEndpoint(
private static RouteEndpoint CreateSubsitutedEndpoint(
string template,
object defaults = null,
object constraints = null,
object requiredValues = null,
params object[] metadata)
{
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)),
0,
new EndpointMetadataCollection(metadata),
"test");
var routePattern = RoutePatternFactory.Parse(template, defaults, constraints);
var policyFactory = CreateParameterPolicyFactory();
var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory);
routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues);
return EndpointFactory.CreateRouteEndpoint(routePattern, metadata: metadata);
}
public static RoutePattern CreateRoutePattern(RoutePattern routePattern, object requiredValues)
{
if (requiredValues != null)
{
var policyFactory = CreateParameterPolicyFactory();
var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory);
routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues);
}
return routePattern;
}
private static DefaultParameterPolicyFactory CreateParameterPolicyFactory()
{
var serviceCollection = new ServiceCollection();
var policyFactory = new DefaultParameterPolicyFactory(
Options.Create(new RouteOptions
{
ConstraintMap =
{
["slugify"] = typeof(SlugifyParameterTransformer),
["upper-case"] = typeof(UpperCaseParameterTransform)
}
}),
serviceCollection.BuildServiceProvider());
return policyFactory;
}
private static RouteEndpoint CreateEndpoint(
string template,
object defaults = null,
object constraints = null,
object requiredValues = null,
params object[] metadata)
{
return EndpointFactory.CreateRouteEndpoint(template, defaults, constraints, requiredValues, metadata: metadata);
}
private class TestMetadata1

View File

@ -3,10 +3,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
@ -19,14 +21,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
// so we're reusing the services here.
public class DfaMatcherTest
{
private RouteEndpoint CreateEndpoint(string template, int order, object defaults = null, EndpointMetadataCollection metadata = null)
private RouteEndpoint CreateEndpoint(string template, int order, object defaults = null, object requiredValues = null)
{
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
order,
metadata ?? EndpointMetadataCollection.Empty,
template);
return EndpointFactory.CreateRouteEndpoint(template, defaults, requiredValues: requiredValues, order: order, displayName: template);
}
private Matcher CreateDfaMatcher(
@ -38,7 +35,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
var serviceCollection = new ServiceCollection()
.AddLogging()
.AddOptions()
.AddRouting();
.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
if (policies != null)
{
@ -106,6 +106,267 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.Null(context.Endpoint);
}
[Fact]
public async Task MatchAsync_RequireValuesAndDefaultValues_EndpointMatched()
{
// Arrange
var endpoint = CreateEndpoint(
"{controller=Home}/{action=Index}/{id?}",
0,
requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null });
var dataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
endpoint
});
var matcher = CreateDfaMatcher(dataSource);
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Same(endpoint, context.Endpoint);
Assert.Collection(
context.RouteValues.OrderBy(kvp => kvp.Key),
(kvp) =>
{
Assert.Equal("action", kvp.Key);
Assert.Equal("Index", kvp.Value);
},
(kvp) =>
{
Assert.Equal("controller", kvp.Key);
Assert.Equal("Home", kvp.Value);
});
}
[Fact]
public async Task MatchAsync_RequireValuesAndDifferentPath_NoEndpointMatched()
{
// Arrange
var endpoint = CreateEndpoint(
"{controller}/{action}",
0,
requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null });
var dataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
endpoint
});
var matcher = CreateDfaMatcher(dataSource);
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/Login/Index";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Null(context.Endpoint);
}
[Fact]
public async Task MatchAsync_RequireValuesAndOptionalParameter_EndpointMatched()
{
// Arrange
var endpoint = CreateEndpoint(
"{controller}/{action}/{id?}",
0,
requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null });
var dataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
endpoint
});
var matcher = CreateDfaMatcher(dataSource);
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/Home/Index/123";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Same(endpoint, context.Endpoint);
Assert.Collection(
context.RouteValues.OrderBy(kvp => kvp.Key),
(kvp) =>
{
Assert.Equal("action", kvp.Key);
Assert.Equal("Index", kvp.Value);
},
(kvp) =>
{
Assert.Equal("controller", kvp.Key);
Assert.Equal("Home", kvp.Value);
},
(kvp) =>
{
Assert.Equal("id", kvp.Key);
Assert.Equal("123", kvp.Value);
});
}
[Theory]
[InlineData("/")]
[InlineData("/TestController")]
[InlineData("/TestController/TestAction")]
[InlineData("/TestController/TestAction/17")]
[InlineData("/TestController/TestAction/17/catchAll")]
public async Task MatchAsync_ShortenedPattern_EndpointMatched(string path)
{
// Arrange
var endpoint = CreateEndpoint(
"{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}",
0,
requiredValues: new { controller = "TestController", action = "TestAction", area = (string)null, page = (string)null });
var dataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
endpoint
});
var matcher = CreateDfaMatcher(dataSource);
var (httpContext, context) = CreateContext();
httpContext.Request.Path = path;
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Same(endpoint, context.Endpoint);
Assert.Equal("TestAction", context.RouteValues["action"]);
Assert.Equal("TestController", context.RouteValues["controller"]);
Assert.Equal("17", context.RouteValues["id"]);
}
[Fact]
public async Task MatchAsync_MultipleEndpointsWithDifferentRequiredValues_EndpointMatched()
{
// Arrange
var endpoint1 = CreateEndpoint(
"{controller}/{action}/{id?}",
0,
requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null });
var endpoint2 = CreateEndpoint(
"{controller}/{action}/{id?}",
0,
requiredValues: new { controller = "Login", action = "Index", area = (string)null, page = (string)null });
var dataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
endpoint1,
endpoint2
});
var matcher = CreateDfaMatcher(dataSource);
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/Home/Index/123";
// Act 1
await matcher.MatchAsync(httpContext, context);
// Assert 1
Assert.Same(endpoint1, context.Endpoint);
httpContext.Request.Path = "/Login/Index/123";
// Act 2
await matcher.MatchAsync(httpContext, context);
// Assert 2
Assert.Same(endpoint2, context.Endpoint);
}
[Fact]
public async Task MatchAsync_ParameterTransformer_EndpointMatched()
{
// Arrange
var endpoint = CreateEndpoint(
"ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}",
0,
requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null });
var dataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
endpoint
});
var matcher = CreateDfaMatcher(dataSource);
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/ConventionalTransformerRoute/conventional-transformer/Index";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Same(endpoint, context.Endpoint);
Assert.Collection(
context.RouteValues.OrderBy(kvp => kvp.Key),
(kvp) =>
{
Assert.Equal("action", kvp.Key);
Assert.Equal("Index", kvp.Value);
},
(kvp) =>
{
Assert.Equal("controller", kvp.Key);
Assert.Equal("ConventionalTransformer", kvp.Value);
});
}
[Fact]
public async Task MatchAsync_DifferentDefaultCase_RouteValueUsesDefaultCase()
{
// Arrange
var endpoint = CreateEndpoint(
"{controller}/{action=TESTACTION}/{id?}",
0,
requiredValues: new { controller = "TestController", action = "TestAction" });
var dataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
endpoint
});
var matcher = CreateDfaMatcher(dataSource);
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/TestController";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Same(endpoint, context.Endpoint);
Assert.Collection(
context.RouteValues.OrderBy(kvp => kvp.Key),
(kvp) =>
{
Assert.Equal("action", kvp.Key);
Assert.Equal("TESTACTION", kvp.Value);
},
(kvp) =>
{
Assert.Equal("controller", kvp.Key);
Assert.Equal("TestController", kvp.Value);
});
}
[Fact]
public async Task MatchAsync_DuplicateTemplatesAndDifferentOrder_LowerOrderEndpointMatched()
{

View File

@ -335,5 +335,24 @@ namespace Microsoft.AspNetCore.Routing.Patterns
kvp => Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
}
[Fact]
public void SubstituteRequiredValues_NullRequiredValueParameter_Fail()
{
// Arrange
var template = "PageRoute/Attribute/{page}";
var defaults = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Null(actual);
}
}
}

View File

@ -13,7 +13,9 @@ namespace Microsoft.AspNetCore.Routing
[Fact]
public void DebuggerToString_NoNameAndRequiredValues_ReturnsString()
{
#pragma warning disable CS0618 // Type or member is obsolete
var metadata = new RouteValuesAddressMetadata(null, new Dictionary<string, object>());
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Equal("Name: - Required values: ", metadata.DebuggerToString());
}
@ -21,11 +23,13 @@ namespace Microsoft.AspNetCore.Routing
[Fact]
public void DebuggerToString_HasNameAndRequiredValues_ReturnsString()
{
#pragma warning disable CS0618 // Type or member is obsolete
var metadata = new RouteValuesAddressMetadata("Name!", new Dictionary<string, object>
{
["requiredValue1"] = "One",
["requiredValue2"] = 2,
});
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Equal("Name: Name! - Required values: requiredValue1 = \"One\", requiredValue2 = \"2\"", metadata.DebuggerToString());
}

View File

@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing
public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
{
// Arrange 1
var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { });
var endpoint1 = CreateEndpoint("/a", routeName: "a");
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", metadataRequiredValues: new { });
var endpoint2 = CreateEndpoint("/b", routeName: "b");
// Act 2
// Trigger change
dynamicDataSource.AddEndpoint(endpoint2);
// Arrange 2
var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { });
var endpoint3 = CreateEndpoint("/c", routeName: "c");
// Act 2
// Trigger change
dynamicDataSource.AddEndpoint(endpoint3);
// Arrange 3
var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { });
var endpoint4 = CreateEndpoint("/d", routeName: "d");
// Act 3
// Trigger change
@ -280,7 +280,7 @@ namespace Microsoft.AspNetCore.Routing
var expected = CreateEndpoint(
"api/orders/{id}",
defaults: new { controller = "Orders", action = "GetById" },
routePatternRequiredValues: new { controller = "Orders", action = "GetById" });
metadataRequiredValues: new { controller = "Orders", action = "GetById" });
var addressScheme = CreateAddressScheme(expected);
// Act
@ -346,7 +346,7 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint(
"/a",
metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteValuesAddressMetadata(string.Empty), });
metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteNameMetadata(string.Empty), });
// Act
var addressScheme = CreateAddressScheme(endpoint);
@ -369,7 +369,6 @@ namespace Microsoft.AspNetCore.Routing
string template,
object defaults = null,
object metadataRequiredValues = null,
object routePatternRequiredValues = null,
int order = 0,
string routeName = null,
EndpointMetadataCollection metadataCollection = null)
@ -377,16 +376,16 @@ namespace Microsoft.AspNetCore.Routing
if (metadataCollection == null)
{
var metadata = new List<object>();
if (!string.IsNullOrEmpty(routeName) || metadataRequiredValues != null)
if (!string.IsNullOrEmpty(routeName))
{
metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(metadataRequiredValues)));
metadata.Add(new RouteNameMetadata(routeName));
}
metadataCollection = new EndpointMetadataCollection(metadata);
}
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues),
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: metadataRequiredValues),
order,
metadataCollection,
null);

View File

@ -3,6 +3,7 @@
using System;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Template
{
@ -23,5 +24,20 @@ namespace Microsoft.AspNetCore.Routing.Template
var parsed = RoutePatternFactory.Parse(template);
return func(parsed);
}
[Fact]
public void InboundPrecedence_ParameterWithRequiredValue_HasPrecedence()
{
var parameterPrecedence = RoutePatternFactory.Parse(
"{controller}").InboundPrecedence;
var requiredValueParameterPrecedence = RoutePatternFactory.Parse(
"{controller}",
defaults: null,
parameterPolicies: null,
requiredValues: new { controller = "Home" }).InboundPrecedence;
Assert.True(requiredValueParameterPrecedence < parameterPrecedence);
}
}
}

View File

@ -1304,7 +1304,11 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
var binder = new TemplateBinder(
UrlEncoder.Default,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePatternFactory.Parse(template),
RoutePatternFactory.Parse(
template,
defaults,
parameterPolicies: null,
requiredValues: new { area = (string)null, action = "Param", controller = "ConventionalTransformer", page = (string)null }),
defaults,
requiredKeys: defaults.Keys,
parameterPolicies: new (string, IParameterPolicy)[] { ("param", new LengthRouteConstraint(500)), ("param", new SlugifyParameterTransformer()), });
@ -1317,6 +1321,96 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
Assert.Equal(expected, boundTemplate);
}
[Fact]
public void BindValues_AmbientAndExplicitValuesDoNotMatch_Success()
{
// Arrange
var expected = "/Travel/Flight";
var template = "{area}/{controller}/{action}";
var defaults = new RouteValueDictionary(new { action = "Index" });
var ambientValues = new RouteValueDictionary(new { area = "Travel", controller = "Rail", action = "Index" });
var explicitValues = new RouteValueDictionary(new { controller = "Flight", action = "Index" });
var binder = new TemplateBinder(
UrlEncoder.Default,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePatternFactory.Parse(
template,
defaults,
parameterPolicies: null,
requiredValues: new { area = "Travel", action = "SomeAction", controller = "Flight", page = (string)null }),
defaults,
requiredKeys: new string[] { "area", "action", "controller", "page" },
parameterPolicies: null);
// Act
var result = binder.GetValues(ambientValues, explicitValues);
var boundTemplate = binder.BindValues(result.AcceptedValues);
// Assert
Assert.Equal(expected, boundTemplate);
}
[Fact]
public void BindValues_LinkingFromPageToAController_Success()
{
// Arrange
var expected = "/LG2/SomeAction";
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new RouteValueDictionary();
var ambientValues = new RouteValueDictionary(new { page = "/LGAnotherPage", id = "17" });
var explicitValues = new RouteValueDictionary(new { controller = "LG2", action = "SomeAction" });
var binder = new TemplateBinder(
UrlEncoder.Default,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePatternFactory.Parse(
template,
defaults,
parameterPolicies: null,
requiredValues: new { area = (string)null, action = "SomeAction", controller = "LG2", page = (string)null }),
defaults,
requiredKeys: new string[] { "area", "action", "controller", "page" },
parameterPolicies: null);
// Act
var result = binder.GetValues(ambientValues, explicitValues);
var boundTemplate = binder.BindValues(result.AcceptedValues);
// Assert
Assert.Equal(expected, boundTemplate);
}
[Fact]
public void BindValues_HasUnmatchingAmbientValues_Discard()
{
// Arrange
var expected = "/Admin/LG3/SomeAction?anothervalue=5";
var template = "Admin/LG3/SomeAction/{id?}";
var defaults = new RouteValueDictionary(new { controller = "LG3", action = "SomeAction", area = "Admin" });
var ambientValues = new RouteValueDictionary(new { controller = "LG1", action = "LinkToAnArea", id = "17" });
var explicitValues = new RouteValueDictionary(new { controller = "LG3", area = "Admin", action = "SomeAction", anothervalue = "5" });
var binder = new TemplateBinder(
UrlEncoder.Default,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePatternFactory.Parse(
template,
defaults,
parameterPolicies: null,
requiredValues: new { area = "Admin", action = "SomeAction", controller = "LG3", page = (string)null }),
defaults,
requiredKeys: new string[] { "area", "action", "controller", "page" },
parameterPolicies: null);
// Act
var result = binder.GetValues(ambientValues, explicitValues);
var boundTemplate = binder.BindValues(result.AcceptedValues);
// Assert
Assert.Equal(expected, boundTemplate);
}
private static IInlineConstraintResolver GetInlineConstraintResolver()
{
var services = new ServiceCollection().AddOptions();

View File

@ -0,0 +1,13 @@
// 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.TestObjects
{
public class UpperCaseParameterTransform : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
return value?.ToString()?.ToUpperInvariant();
}
}
}

View File

@ -98,7 +98,7 @@ namespace RoutingWebSite
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
},
new RouteValuesAddressMetadata(routeName: "WithSingleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
new RouteNameMetadata(routeName: "WithSingleAsteriskCatchAll"));
routes.MapGet(
"/WithDoubleAsteriskCatchAll/{**path}",
(httpContext) =>
@ -111,7 +111,7 @@ namespace RoutingWebSite
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
},
new RouteValuesAddressMetadata(routeName: "WithDoubleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
new RouteNameMetadata(routeName: "WithDoubleAsteriskCatchAll"));
});
app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));