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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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.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();
|
||||
|
|
|
|||
|
|
@ -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: ");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
// 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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
updatedDefaults ?? original.Defaults,
|
||||
original.ParameterPolicies,
|
||||
requiredValues,
|
||||
updatedParameters ?? original.Parameters,
|
||||
updatedParameters ?? original.Parameters,
|
||||
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.
|
||||
/// </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 =
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"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"));
|
||||
|
|
|
|||
Loading…
Reference in New Issue