Support RoutePattern required values in matcher and link generator
This commit is contained in:
parent
142b6a73ec
commit
96cd055e05
|
|
@ -116,11 +116,14 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
params object[] metadata)
|
params object[] metadata)
|
||||||
{
|
{
|
||||||
var endpointMetadata = new List<object>(metadata ?? Array.Empty<object>());
|
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(
|
return new RouteEndpoint(
|
||||||
(context) => Task.CompletedTask,
|
(context) => Task.CompletedTask,
|
||||||
RoutePatternFactory.Parse(template, defaults, constraints),
|
RoutePatternFactory.Parse(template, defaults, constraints, requiredValues),
|
||||||
order,
|
order,
|
||||||
new EndpointMetadataCollection(endpointMetadata),
|
new EndpointMetadataCollection(endpointMetadata),
|
||||||
displayName);
|
displayName);
|
||||||
|
|
@ -139,16 +142,13 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint)
|
protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint)
|
||||||
{
|
{
|
||||||
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
|
||||||
var requiredValues = routeValuesAddressMetadata?.RequiredValues ?? new RouteValueDictionary();
|
|
||||||
|
|
||||||
treeRouteBuilder.MapOutbound(
|
treeRouteBuilder.MapOutbound(
|
||||||
NullRouter.Instance,
|
NullRouter.Instance,
|
||||||
new RouteTemplate(RoutePatternFactory.Parse(
|
new RouteTemplate(RoutePatternFactory.Parse(
|
||||||
endpoint.RoutePattern.RawText,
|
endpoint.RoutePattern.RawText,
|
||||||
defaults: endpoint.RoutePattern.Defaults,
|
defaults: endpoint.RoutePattern.Defaults,
|
||||||
parameterPolicies: null)),
|
parameterPolicies: null)),
|
||||||
requiredLinkValues: new RouteValueDictionary(requiredValues),
|
requiredLinkValues: new RouteValueDictionary(endpoint.RoutePattern.RequiredValues),
|
||||||
routeName: null,
|
routeName: null,
|
||||||
order: 0);
|
order: 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,9 +10,8 @@ using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using RoutingSandbox.Framework;
|
||||||
|
|
||||||
namespace RoutingSandbox
|
namespace RoutingSandbox
|
||||||
{
|
{
|
||||||
|
|
@ -23,7 +22,11 @@ namespace RoutingSandbox
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddRouting();
|
services.AddRouting(options =>
|
||||||
|
{
|
||||||
|
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
|
||||||
|
});
|
||||||
|
services.AddSingleton<FrameworkEndpointDataSource>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
|
|
@ -72,12 +75,22 @@ namespace RoutingSandbox
|
||||||
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
|
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
|
||||||
{
|
{
|
||||||
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
|
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
|
||||||
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
|
var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
|
||||||
graphWriter.Write(dataSource, writer);
|
graphWriter.Write(dataSource, writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
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();
|
app.UseStaticFiles();
|
||||||
|
|
|
||||||
|
|
@ -167,13 +167,14 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
sb.Append(", Defaults: new { ");
|
sb.Append(", Defaults: new { ");
|
||||||
sb.Append(string.Join(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)));
|
sb.Append(string.Join(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)));
|
||||||
sb.Append(" }");
|
sb.Append(" }");
|
||||||
var routeValuesAddressMetadata = routeEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
var routeNameMetadata = routeEndpoint.Metadata.GetMetadata<IRouteNameMetadata>();
|
||||||
sb.Append(", Route Name: ");
|
sb.Append(", Route Name: ");
|
||||||
sb.Append(routeValuesAddressMetadata?.RouteName);
|
sb.Append(routeNameMetadata?.RouteName);
|
||||||
if (routeValuesAddressMetadata?.RequiredValues != null)
|
var routeValues = routeEndpoint.RoutePattern.RequiredValues;
|
||||||
|
if (routeValues.Count > 0)
|
||||||
{
|
{
|
||||||
sb.Append(", Required Values: new { ");
|
sb.Append(", Required Values: new { ");
|
||||||
sb.Append(string.Join(", ", FormatValues(routeValuesAddressMetadata.RequiredValues)));
|
sb.Append(string.Join(", ", FormatValues(routeValues)));
|
||||||
sb.Append(" }");
|
sb.Append(" }");
|
||||||
}
|
}
|
||||||
sb.Append(", Order: ");
|
sb.Append(", Order: ");
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
_uriBuildingContextPool,
|
_uriBuildingContextPool,
|
||||||
endpoint.RoutePattern,
|
endpoint.RoutePattern,
|
||||||
new RouteValueDictionary(endpoint.RoutePattern.Defaults),
|
new RouteValueDictionary(endpoint.RoutePattern.Defaults),
|
||||||
endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>()?.RequiredValues.Keys,
|
endpoint.RoutePattern.RequiredValues.Keys,
|
||||||
policies);
|
policies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
@ -9,6 +10,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
/// Represents metadata used during link generation to find
|
/// Represents metadata used during link generation to find
|
||||||
/// the associated endpoint using route values.
|
/// the associated endpoint using route values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Route values are now specified on a RoutePattern.")]
|
||||||
public interface IRouteValuesAddressMetadata
|
public interface IRouteValuesAddressMetadata
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -143,24 +143,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
{
|
{
|
||||||
var parent = parents[j];
|
var parent = parents[j];
|
||||||
var part = segment.Parts[0];
|
var part = segment.Parts[0];
|
||||||
|
var parameterPart = part as RoutePatternParameterPart;
|
||||||
if (segment.IsSimple && part is RoutePatternLiteralPart literalPart)
|
if (segment.IsSimple && part is RoutePatternLiteralPart literalPart)
|
||||||
{
|
{
|
||||||
DfaNode next = null;
|
AddLiteralNode(includeLabel, nextParents, parent, literalPart.Content);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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
|
// 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
|
// 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);
|
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)
|
if (parent.Parameters == null)
|
||||||
{
|
{
|
||||||
|
|
@ -254,6 +264,23 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
return root;
|
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)
|
private RoutePatternPathSegment GetCurrentSegment(RouteEndpoint endpoint, int depth)
|
||||||
{
|
{
|
||||||
if (depth < endpoint.RoutePattern.PathSegments.Count)
|
if (depth < endpoint.RoutePattern.PathSegments.Count)
|
||||||
|
|
@ -500,11 +527,25 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
slotIndex = _assignments.Count;
|
slotIndex = _assignments.Count;
|
||||||
_assignments.Add(parameterPart.Name, slotIndex);
|
_assignments.Add(parameterPart.Name, slotIndex);
|
||||||
|
|
||||||
var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll;
|
// A parameter can have a required value, default value/catch all, or be a normal parameter
|
||||||
_slots.Add(hasDefaultValue ? new KeyValuePair<string, object>(parameterPart.Name, parameterPart.Default) : default);
|
// 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);
|
catchAll = (parameterPart.Name, i, slotIndex);
|
||||||
}
|
}
|
||||||
|
|
@ -720,5 +761,15 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
return (nodeBuilderPolicies.ToArray(), endpointComparerPolicies.ToArray(), endpointSelectorPolicies.ToArray());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
updatedDefaults ?? original.Defaults,
|
updatedDefaults ?? original.Defaults,
|
||||||
original.ParameterPolicies,
|
original.ParameterPolicies,
|
||||||
requiredValues,
|
requiredValues,
|
||||||
updatedParameters ?? original.Parameters,
|
updatedParameters ?? original.Parameters,
|
||||||
updatedSegments ?? original.PathSegments);
|
updatedSegments ?? original.PathSegments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
/// Metadata used during link generation to find the associated endpoint using route values.
|
/// Metadata used during link generation to find the associated endpoint using route values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||||
|
[Obsolete("Route values are now specified on a RoutePattern.")]
|
||||||
public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata
|
public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata
|
||||||
{
|
{
|
||||||
private static readonly IReadOnlyDictionary<string, object> EmptyRouteValues =
|
private static readonly IReadOnlyDictionary<string, object> EmptyRouteValues =
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
|
var metadata = endpoint.Metadata.GetMetadata<IRouteNameMetadata>();
|
||||||
if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0)
|
if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -137,8 +137,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = CreateOutboundRouteEntry(
|
var entry = CreateOutboundRouteEntry(
|
||||||
routeEndpoint,
|
routeEndpoint,
|
||||||
metadata?.RequiredValues ?? routeEndpoint.RoutePattern.RequiredValues,
|
routeEndpoint.RoutePattern.RequiredValues,
|
||||||
metadata?.RouteName);
|
metadata?.RouteName);
|
||||||
|
|
||||||
var outboundMatch = new OutboundMatch() { Entry = entry };
|
var outboundMatch = new OutboundMatch() { Entry = entry };
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
var segment = routePattern.PathSegments[i];
|
var segment = routePattern.PathSegments[i];
|
||||||
|
|
||||||
var digit = ComputeInboundPrecedenceDigit(segment);
|
var digit = ComputeInboundPrecedenceDigit(routePattern, segment);
|
||||||
Debug.Assert(digit >= 0 && digit < 10);
|
Debug.Assert(digit >= 0 && digit < 10);
|
||||||
|
|
||||||
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
|
precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
|
||||||
|
|
@ -217,7 +217,9 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
// see description on ComputeInboundPrecedenceDigit(TemplateSegment segment)
|
// 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)
|
if (pathSegment.Parts.Count > 1)
|
||||||
{
|
{
|
||||||
|
|
@ -233,6 +235,13 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
}
|
}
|
||||||
else if (part is RoutePatternParameterPart parameterPart)
|
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;
|
var digit = parameterPart.IsCatchAll ? 5 : 3;
|
||||||
|
|
||||||
// If there is a route constraint for the parameter, reduce order by 1
|
// If there is a route constraint for the parameter, reduce order by 1
|
||||||
|
|
|
||||||
|
|
@ -174,11 +174,23 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
ambientValueProcessedCount++;
|
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
|
if (!_pattern.RequiredValues.TryGetValue(key, out var requiredValue))
|
||||||
// to also be filters, which are tracked when we populate 'slots'.
|
{
|
||||||
if (!RoutePartsEqual(value, ambientValue))
|
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;
|
copyAmbientValues = false;
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Constraints;
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.AspNetCore.Routing.TestObjects;
|
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
|
||||||
|
|
@ -309,14 +309,6 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path);
|
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]
|
[Fact]
|
||||||
public void GetLink_ParameterTransformer()
|
public void GetLink_ParameterTransformer()
|
||||||
{
|
{
|
||||||
|
|
@ -346,7 +338,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
// Arrange
|
// Arrange
|
||||||
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
||||||
"{controller:upper-case}/{name}",
|
"{controller:upper-case}/{name}",
|
||||||
requiredValues: new { controller = "Home", name = "Test", c = "hithere", },
|
requiredValues: new { controller = "Home", name = "Test", },
|
||||||
policies: new { c = new UpperCaseParameterTransform(), });
|
policies: new { c = new UpperCaseParameterTransform(), });
|
||||||
|
|
||||||
Action<IServiceCollection> configure = (s) =>
|
Action<IServiceCollection> configure = (s) =>
|
||||||
|
|
@ -586,24 +578,24 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
"Home/Index",
|
"Home/Index",
|
||||||
order: 3,
|
order: 3,
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpointController = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home",
|
"Home",
|
||||||
order: 2,
|
order: 2,
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpointEmpty = EndpointFactory.CreateRouteEndpoint(
|
||||||
"",
|
"",
|
||||||
order: 1,
|
order: 1,
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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
|
// This endpoint should be used to generate the link when an id is present
|
||||||
var endpointControllerActionParameter = EndpointFactory.CreateRouteEndpoint(
|
var endpointControllerActionParameter = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
order: 0,
|
order: 0,
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(endpointControllerAction, endpointController, endpointEmpty, endpointControllerActionParameter);
|
||||||
|
|
||||||
|
|
@ -642,11 +634,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var homeIndex = EndpointFactory.CreateRouteEndpoint(
|
var homeIndex = EndpointFactory.CreateRouteEndpoint(
|
||||||
"{controller}/{action}/{id?}",
|
"{controller}/{action}/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var homeLogin = EndpointFactory.CreateRouteEndpoint(
|
||||||
"{controller}/{action}/{id?}",
|
"{controller}/{action}/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin);
|
||||||
|
|
||||||
|
|
@ -685,11 +677,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var homeIndex = EndpointFactory.CreateRouteEndpoint(
|
var homeIndex = EndpointFactory.CreateRouteEndpoint(
|
||||||
"{controller}/{action}/{id?}",
|
"{controller}/{action}/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var homeLogin = EndpointFactory.CreateRouteEndpoint(
|
||||||
"{controller}/{action}/{id?}",
|
"{controller}/{action}/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,13 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Matching;
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
|
|
@ -20,17 +24,22 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
string displayName = null,
|
string displayName = null,
|
||||||
params object[] metadata)
|
params object[] metadata)
|
||||||
{
|
{
|
||||||
var d = new List<object>(metadata ?? Array.Empty<object>());
|
var routePattern = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
|
||||||
if (requiredValues != null)
|
|
||||||
{
|
|
||||||
d.Add(new RouteValuesAddressMetadata(new RouteValueDictionary(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(
|
return new RouteEndpoint(
|
||||||
TestConstants.EmptyRequestDelegate,
|
TestConstants.EmptyRequestDelegate,
|
||||||
RoutePatternFactory.Parse(template, defaults, policies),
|
routePattern,
|
||||||
order,
|
order,
|
||||||
new EndpointMetadataCollection(d),
|
new EndpointMetadataCollection(metadata ?? Array.Empty<object>()),
|
||||||
displayName);
|
displayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -86,11 +86,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -116,11 +116,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -145,11 +145,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
@ -177,11 +177,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
var endpoint1 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id}",
|
"Home/Index/{id}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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(
|
var endpoint2 = EndpointFactory.CreateRouteEndpoint(
|
||||||
"Home/Index/{id?}",
|
"Home/Index/{id?}",
|
||||||
defaults: new { controller = "Home", action = "Index", },
|
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);
|
var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Constraints;
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
@ -727,6 +729,253 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
Assert.Null(a.PolicyEdges);
|
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]
|
[Fact]
|
||||||
public void CreateCandidate_JustLiterals()
|
public void CreateCandidate_JustLiterals()
|
||||||
{
|
{
|
||||||
|
|
@ -971,26 +1220,70 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies)
|
private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies)
|
||||||
{
|
{
|
||||||
|
var policyFactory = CreateParameterPolicyFactory();
|
||||||
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
|
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
|
||||||
return new DfaMatcherBuilder(
|
return new DfaMatcherBuilder(
|
||||||
NullLoggerFactory.Instance,
|
NullLoggerFactory.Instance,
|
||||||
new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), Mock.Of<IServiceProvider>()),
|
policyFactory,
|
||||||
Mock.Of<EndpointSelector>(),
|
Mock.Of<EndpointSelector>(),
|
||||||
policies);
|
policies);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RouteEndpoint CreateEndpoint(
|
private static RouteEndpoint CreateSubsitutedEndpoint(
|
||||||
string template,
|
string template,
|
||||||
object defaults = null,
|
object defaults = null,
|
||||||
object constraints = null,
|
object constraints = null,
|
||||||
|
object requiredValues = null,
|
||||||
params object[] metadata)
|
params object[] metadata)
|
||||||
{
|
{
|
||||||
return new RouteEndpoint(
|
var routePattern = RoutePatternFactory.Parse(template, defaults, constraints);
|
||||||
TestConstants.EmptyRequestDelegate,
|
|
||||||
RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)),
|
var policyFactory = CreateParameterPolicyFactory();
|
||||||
0,
|
var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory);
|
||||||
new EndpointMetadataCollection(metadata),
|
|
||||||
"test");
|
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
|
private class TestMetadata1
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
|
@ -19,14 +21,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
// so we're reusing the services here.
|
// so we're reusing the services here.
|
||||||
public class DfaMatcherTest
|
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(
|
return EndpointFactory.CreateRouteEndpoint(template, defaults, requiredValues: requiredValues, order: order, displayName: template);
|
||||||
TestConstants.EmptyRequestDelegate,
|
|
||||||
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
|
|
||||||
order,
|
|
||||||
metadata ?? EndpointMetadataCollection.Empty,
|
|
||||||
template);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Matcher CreateDfaMatcher(
|
private Matcher CreateDfaMatcher(
|
||||||
|
|
@ -38,7 +35,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
var serviceCollection = new ServiceCollection()
|
var serviceCollection = new ServiceCollection()
|
||||||
.AddLogging()
|
.AddLogging()
|
||||||
.AddOptions()
|
.AddOptions()
|
||||||
.AddRouting();
|
.AddRouting(options =>
|
||||||
|
{
|
||||||
|
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
|
||||||
|
});
|
||||||
|
|
||||||
if (policies != null)
|
if (policies != null)
|
||||||
{
|
{
|
||||||
|
|
@ -106,6 +106,267 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
Assert.Null(context.Endpoint);
|
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]
|
[Fact]
|
||||||
public async Task MatchAsync_DuplicateTemplatesAndDifferentOrder_LowerOrderEndpointMatched()
|
public async Task MatchAsync_DuplicateTemplatesAndDifferentOrder_LowerOrderEndpointMatched()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>("area", "Admin"), kvp),
|
||||||
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DebuggerToString_NoNameAndRequiredValues_ReturnsString()
|
public void DebuggerToString_NoNameAndRequiredValues_ReturnsString()
|
||||||
{
|
{
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
var metadata = new RouteValuesAddressMetadata(null, new Dictionary<string, object>());
|
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());
|
Assert.Equal("Name: - Required values: ", metadata.DebuggerToString());
|
||||||
}
|
}
|
||||||
|
|
@ -21,11 +23,13 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DebuggerToString_HasNameAndRequiredValues_ReturnsString()
|
public void DebuggerToString_HasNameAndRequiredValues_ReturnsString()
|
||||||
{
|
{
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
var metadata = new RouteValuesAddressMetadata("Name!", new Dictionary<string, object>
|
var metadata = new RouteValuesAddressMetadata("Name!", new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["requiredValue1"] = "One",
|
["requiredValue1"] = "One",
|
||||||
["requiredValue2"] = 2,
|
["requiredValue2"] = 2,
|
||||||
});
|
});
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
Assert.Equal("Name: Name! - Required values: requiredValue1 = \"One\", requiredValue2 = \"2\"", metadata.DebuggerToString());
|
Assert.Equal("Name: Name! - Required values: requiredValue1 = \"One\", requiredValue2 = \"2\"", metadata.DebuggerToString());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
|
public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
|
||||||
{
|
{
|
||||||
// Arrange 1
|
// Arrange 1
|
||||||
var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { });
|
var endpoint1 = CreateEndpoint("/a", routeName: "a");
|
||||||
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
||||||
|
|
||||||
// Act 1
|
// Act 1
|
||||||
|
|
@ -93,21 +93,21 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
Assert.Same(endpoint1, actual);
|
Assert.Same(endpoint1, actual);
|
||||||
|
|
||||||
// Arrange 2
|
// Arrange 2
|
||||||
var endpoint2 = CreateEndpoint("/b", metadataRequiredValues: new { });
|
var endpoint2 = CreateEndpoint("/b", routeName: "b");
|
||||||
|
|
||||||
// Act 2
|
// Act 2
|
||||||
// Trigger change
|
// Trigger change
|
||||||
dynamicDataSource.AddEndpoint(endpoint2);
|
dynamicDataSource.AddEndpoint(endpoint2);
|
||||||
|
|
||||||
// Arrange 2
|
// Arrange 2
|
||||||
var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { });
|
var endpoint3 = CreateEndpoint("/c", routeName: "c");
|
||||||
|
|
||||||
// Act 2
|
// Act 2
|
||||||
// Trigger change
|
// Trigger change
|
||||||
dynamicDataSource.AddEndpoint(endpoint3);
|
dynamicDataSource.AddEndpoint(endpoint3);
|
||||||
|
|
||||||
// Arrange 3
|
// Arrange 3
|
||||||
var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { });
|
var endpoint4 = CreateEndpoint("/d", routeName: "d");
|
||||||
|
|
||||||
// Act 3
|
// Act 3
|
||||||
// Trigger change
|
// Trigger change
|
||||||
|
|
@ -280,7 +280,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var expected = CreateEndpoint(
|
var expected = CreateEndpoint(
|
||||||
"api/orders/{id}",
|
"api/orders/{id}",
|
||||||
defaults: new { controller = "Orders", action = "GetById" },
|
defaults: new { controller = "Orders", action = "GetById" },
|
||||||
routePatternRequiredValues: new { controller = "Orders", action = "GetById" });
|
metadataRequiredValues: new { controller = "Orders", action = "GetById" });
|
||||||
var addressScheme = CreateAddressScheme(expected);
|
var addressScheme = CreateAddressScheme(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -346,7 +346,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
// Arrange
|
// Arrange
|
||||||
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
var endpoint = EndpointFactory.CreateRouteEndpoint(
|
||||||
"/a",
|
"/a",
|
||||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteValuesAddressMetadata(string.Empty), });
|
metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteNameMetadata(string.Empty), });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var addressScheme = CreateAddressScheme(endpoint);
|
var addressScheme = CreateAddressScheme(endpoint);
|
||||||
|
|
@ -369,7 +369,6 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
string template,
|
string template,
|
||||||
object defaults = null,
|
object defaults = null,
|
||||||
object metadataRequiredValues = null,
|
object metadataRequiredValues = null,
|
||||||
object routePatternRequiredValues = null,
|
|
||||||
int order = 0,
|
int order = 0,
|
||||||
string routeName = null,
|
string routeName = null,
|
||||||
EndpointMetadataCollection metadataCollection = null)
|
EndpointMetadataCollection metadataCollection = null)
|
||||||
|
|
@ -377,16 +376,16 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
if (metadataCollection == null)
|
if (metadataCollection == null)
|
||||||
{
|
{
|
||||||
var metadata = new List<object>();
|
var metadata = new List<object>();
|
||||||
if (!string.IsNullOrEmpty(routeName) || metadataRequiredValues != null)
|
if (!string.IsNullOrEmpty(routeName))
|
||||||
{
|
{
|
||||||
metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(metadataRequiredValues)));
|
metadata.Add(new RouteNameMetadata(routeName));
|
||||||
}
|
}
|
||||||
metadataCollection = new EndpointMetadataCollection(metadata);
|
metadataCollection = new EndpointMetadataCollection(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RouteEndpoint(
|
return new RouteEndpoint(
|
||||||
TestConstants.EmptyRequestDelegate,
|
TestConstants.EmptyRequestDelegate,
|
||||||
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues),
|
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: metadataRequiredValues),
|
||||||
order,
|
order,
|
||||||
metadataCollection,
|
metadataCollection,
|
||||||
null);
|
null);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
|
|
@ -23,5 +24,20 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
var parsed = RoutePatternFactory.Parse(template);
|
var parsed = RoutePatternFactory.Parse(template);
|
||||||
return func(parsed);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1304,7 +1304,11 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
var binder = new TemplateBinder(
|
var binder = new TemplateBinder(
|
||||||
UrlEncoder.Default,
|
UrlEncoder.Default,
|
||||||
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
|
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,
|
defaults,
|
||||||
requiredKeys: defaults.Keys,
|
requiredKeys: defaults.Keys,
|
||||||
parameterPolicies: new (string, IParameterPolicy)[] { ("param", new LengthRouteConstraint(500)), ("param", new SlugifyParameterTransformer()), });
|
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);
|
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()
|
private static IInlineConstraintResolver GetInlineConstraintResolver()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection().AddOptions();
|
var services = new ServiceCollection().AddOptions();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -98,7 +98,7 @@ namespace RoutingWebSite
|
||||||
return response.WriteAsync(
|
return response.WriteAsync(
|
||||||
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
|
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
|
||||||
},
|
},
|
||||||
new RouteValuesAddressMetadata(routeName: "WithSingleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
|
new RouteNameMetadata(routeName: "WithSingleAsteriskCatchAll"));
|
||||||
routes.MapGet(
|
routes.MapGet(
|
||||||
"/WithDoubleAsteriskCatchAll/{**path}",
|
"/WithDoubleAsteriskCatchAll/{**path}",
|
||||||
(httpContext) =>
|
(httpContext) =>
|
||||||
|
|
@ -111,7 +111,7 @@ namespace RoutingWebSite
|
||||||
return response.WriteAsync(
|
return response.WriteAsync(
|
||||||
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
|
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
|
||||||
},
|
},
|
||||||
new RouteValuesAddressMetadata(routeName: "WithDoubleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
|
new RouteNameMetadata(routeName: "WithDoubleAsteriskCatchAll"));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
|
app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue