Use coventional routes for link generation (#9037)
Use coventional routes for link generation This change enables using conventional routes for link generation when using MVC conventional routes. This change makes MVC link generation behaviour highly compatible with 2.1. The way that this works is that we create endpoints for **MATCHING** using the denormalized conventional route, but we tell those endpoints to suppress link generation. For link generation we generate a non-matching endpoints per-route with the same order value. I added the concept of *required value any* to link generation. This is needed because for an endpoint to participate in link generation using RouteValuesAddress it needs to have some required values. These details are a little fiddly, but I think it's worth doing this feature completely.
This commit is contained in:
parent
896537bb8c
commit
258d34e382
|
|
@ -671,6 +671,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
public sealed partial class RoutePattern
|
||||
{
|
||||
internal RoutePattern() { }
|
||||
public static readonly object RequiredValueAny;
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> Defaults { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public decimal InboundPrecedence { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public decimal OutboundPrecedence { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Routing.DecisionTree;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Internal
|
||||
|
|
@ -21,38 +22,57 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
private static readonly RouteValueDictionary EmptyAmbientValues = new RouteValueDictionary();
|
||||
|
||||
private readonly DecisionTreeNode<OutboundMatch> _root;
|
||||
private readonly Dictionary<string, HashSet<object>> _knownValues;
|
||||
private readonly List<OutboundMatch> _conventionalEntries;
|
||||
|
||||
public LinkGenerationDecisionTree(IReadOnlyList<OutboundMatch> entries)
|
||||
{
|
||||
_root = DecisionTreeBuilder<OutboundMatch>.GenerateTree(
|
||||
entries,
|
||||
new OutboundMatchClassifier());
|
||||
// We split up the entries into:
|
||||
// 1. attribute routes - these go into the tree
|
||||
// 2. conventional routes - these are a list
|
||||
var attributedEntries = new List<OutboundMatch>();
|
||||
_conventionalEntries = new List<OutboundMatch>();
|
||||
|
||||
_knownValues = new Dictionary<string, HashSet<object>>(StringComparer.OrdinalIgnoreCase);
|
||||
// Anything with a RoutePattern.RequiredValueAny as a RequiredValue is a conventional route.
|
||||
// This is because RequiredValueAny acts as a wildcard, whereas an attribute route entry
|
||||
// is denormalized to contain an exact set of required values.
|
||||
//
|
||||
// We will only see conventional routes show up here for endpoint routing.
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var isAttributeRoute = true;
|
||||
var entry = entries[i];
|
||||
foreach (var kvp in entry.Entry.RequiredLinkValues)
|
||||
{
|
||||
if (!_knownValues.TryGetValue(kvp.Key, out var values))
|
||||
if (RoutePattern.IsRequiredValueAny(kvp.Value))
|
||||
{
|
||||
values = new HashSet<object>(RouteValueEqualityComparer.Default);
|
||||
_knownValues.Add(kvp.Key, values);
|
||||
isAttributeRoute = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
values.Add(kvp.Value ?? string.Empty);
|
||||
if (isAttributeRoute)
|
||||
{
|
||||
attributedEntries.Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
_conventionalEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
_root = DecisionTreeBuilder<OutboundMatch>.GenerateTree(
|
||||
attributedEntries,
|
||||
new OutboundMatchClassifier());
|
||||
}
|
||||
|
||||
public IList<OutboundMatchResult> GetMatches(RouteValueDictionary values, RouteValueDictionary ambientValues)
|
||||
{
|
||||
// Perf: Avoid allocation for List if there aren't any Matches or Criteria
|
||||
if (_root.Matches.Count > 0 || _root.Criteria.Count > 0)
|
||||
if (_root.Matches.Count > 0 || _root.Criteria.Count > 0 || _conventionalEntries.Count > 0)
|
||||
{
|
||||
var results = new List<OutboundMatchResult>();
|
||||
Walk(results, values, ambientValues ?? EmptyAmbientValues, _root, isFallbackPath: false);
|
||||
ProcessConventionalEntries(results, values, ambientValues ?? EmptyAmbientValues);
|
||||
results.Sort(OutboundMatchResultComparer.Instance);
|
||||
return results;
|
||||
}
|
||||
|
|
@ -110,19 +130,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
{
|
||||
Walk(results, values, ambientValues, branch, isFallbackPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If an explicitly specified value doesn't match any branch, then speculatively walk the
|
||||
// "null" path if the value doesn't match any known value.
|
||||
//
|
||||
// This can happen when linking from a page <-> action. We want to be
|
||||
// able to use "page" and "action" as normal route parameters.
|
||||
var knownValues = _knownValues[key];
|
||||
if (!knownValues.Contains(value ?? string.Empty) && criterion.Branches.TryGetValue(string.Empty, out branch))
|
||||
{
|
||||
Walk(results, values, ambientValues, branch, isFallbackPath: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -147,6 +154,17 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
}
|
||||
}
|
||||
|
||||
private void ProcessConventionalEntries(
|
||||
List<OutboundMatchResult> results,
|
||||
RouteValueDictionary values,
|
||||
RouteValueDictionary ambientvalues)
|
||||
{
|
||||
for (var i = 0; i < _conventionalEntries.Count; i++)
|
||||
{
|
||||
results.Add(new OutboundMatchResult(_conventionalEntries[i], isFallbackMatch: false));
|
||||
}
|
||||
}
|
||||
|
||||
private class OutboundMatchClassifier : IClassifier<OutboundMatch>
|
||||
{
|
||||
public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// 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.
|
||||
|
||||
using System;
|
||||
|
|
@ -43,8 +43,9 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
{
|
||||
// There are three possible cases here:
|
||||
// 1. Required value is null-ish
|
||||
// 2. Required value corresponds to a parameter
|
||||
// 3. Required value corresponds to a matching default value
|
||||
// 2. Required value is *any*
|
||||
// 3. Required value corresponds to a parameter
|
||||
// 4. Required value corresponds to a matching default value
|
||||
//
|
||||
// If none of these are true then we can reject this substitution.
|
||||
RoutePatternParameterPart parameter;
|
||||
|
|
@ -76,9 +77,28 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
// Ex: {controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... }
|
||||
continue;
|
||||
}
|
||||
else if (RoutePattern.IsRequiredValueAny(kvp.Value))
|
||||
{
|
||||
// 2. Required value is *any* - this is allowed for a parameter with a default, but not
|
||||
// a non-parameter default.
|
||||
if (original.GetParameter(kvp.Key) == null &&
|
||||
original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
|
||||
!RouteValueEqualityComparer.Default.Equals(string.Empty, defaultValue))
|
||||
{
|
||||
// Fail: this route as a non-parameter default that is stricter than *any*.
|
||||
//
|
||||
// Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = *any* }
|
||||
return null;
|
||||
}
|
||||
|
||||
// Success: (for this parameter at least)
|
||||
//
|
||||
// Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = *any*, ... }
|
||||
continue;
|
||||
}
|
||||
else if ((parameter = original.GetParameter(kvp.Key)) != null)
|
||||
{
|
||||
// 2. Required value corresponds to a parameter - check to make sure that this value matches
|
||||
// 3. Required value corresponds to a parameter - check to make sure that this value matches
|
||||
// any IRouteConstraint implementations.
|
||||
if (!MatchesConstraints(original, parameter, kvp.Key, requiredValues))
|
||||
{
|
||||
|
|
@ -96,7 +116,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
|
||||
RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
|
||||
{
|
||||
// 3. Required value corresponds to a matching default value - check to make sure that this value matches
|
||||
// 4. Required value corresponds to a matching default value - check to make sure that this value matches
|
||||
// any IRouteConstraint implementations. It's unlikely that this would happen in practice but it doesn't
|
||||
// hurt for us to check.
|
||||
if (!MatchesConstraints(original, parameter: null, kvp.Key, requiredValues))
|
||||
|
|
@ -142,7 +162,10 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
// We only need to handle the case where the required value maps to a parameter. That's the only
|
||||
// case where we allow a default and a required value to disagree, and we already validated the
|
||||
// other cases.
|
||||
if (parameter != null &&
|
||||
//
|
||||
// If the required value is *any* then don't remove the default.
|
||||
if (parameter != null &&
|
||||
!RoutePattern.IsRequiredValueAny(kvp.Value) &&
|
||||
original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
|
||||
!RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,21 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
[DebuggerDisplay("{DebuggerToString()}")]
|
||||
public sealed class RoutePattern
|
||||
{
|
||||
/// <summary>
|
||||
/// A marker object that can be used in <see cref="RequiredValues"/> to designate that
|
||||
/// any non-null or non-empty value is required.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="RequiredValueAny"/> is only use in routing is in <see cref="RoutePattern.RequiredValues"/>.
|
||||
/// <see cref="RequiredValueAny"/> is not valid as a route value, and will convert to the null/empty string.
|
||||
/// </remarks>
|
||||
public static readonly object RequiredValueAny = new RequiredValueAnySentinal();
|
||||
|
||||
internal static bool IsRequiredValueAny(object value)
|
||||
{
|
||||
return object.ReferenceEquals(RequiredValueAny, value);
|
||||
}
|
||||
|
||||
private const string SeparatorString = "/";
|
||||
|
||||
internal RoutePattern(
|
||||
|
|
@ -140,5 +155,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
{
|
||||
return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString()));
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||
private class RequiredValueAnySentinal
|
||||
{
|
||||
private string DebuggerToString() => "*any*";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,8 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
throw new InvalidOperationException($"Unable to find required value '{key}' on route pattern.");
|
||||
}
|
||||
|
||||
if (!RoutePartsEqual(ambientValue, _pattern.RequiredValues[key]))
|
||||
if (!RoutePartsEqual(ambientValue, _pattern.RequiredValues[key]) &&
|
||||
!RoutePattern.IsRequiredValueAny(_pattern.RequiredValues[key]))
|
||||
{
|
||||
copyAmbientValues = false;
|
||||
break;
|
||||
|
|
@ -261,10 +262,11 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
//
|
||||
// OR in plain English... when linking from a page in an area to an action in the same area, it should
|
||||
// be possible to use the area as an ambient value.
|
||||
if (!copyAmbientValues && _pattern.RequiredValues.TryGetValue(key, out var requiredValue))
|
||||
if (!copyAmbientValues && !hasExplicitValue && _pattern.RequiredValues.TryGetValue(key, out var requiredValue))
|
||||
{
|
||||
hasAmbientValue = ambientValues != null && ambientValues.TryGetValue(key, out ambientValue);
|
||||
if (hasAmbientValue && RoutePartsEqual(requiredValue, ambientValue))
|
||||
if (hasAmbientValue &&
|
||||
(RoutePartsEqual(requiredValue, ambientValue) || RoutePattern.IsRequiredValueAny(requiredValue)))
|
||||
{
|
||||
// Treat this an an explicit value to *force it*.
|
||||
slots[i] = new KeyValuePair<string, object>(key, ambientValue);
|
||||
|
|
|
|||
|
|
@ -583,9 +583,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
matches,
|
||||
m => { Assert.Same(entry1, m); });
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -689,9 +687,7 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing
|
|||
var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
matches,
|
||||
m => { Assert.Same(entry2, m); });
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,714 @@
|
|||
// 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.Http.Features;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
// This is a set of integration tests that are similar to a typical MVC configuration.
|
||||
//
|
||||
// We're doing this here because it's relatively expensive to test these scenarios
|
||||
// inside MVC - it requires creating actual controllers and pages.
|
||||
public class LinkGeneratorIntegrationTest : LinkGeneratorTestBase
|
||||
{
|
||||
public LinkGeneratorIntegrationTest()
|
||||
{
|
||||
var endpoints = new List<Endpoint>()
|
||||
{
|
||||
// Attribute routed endpoint 1
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"api/Pets/{id}",
|
||||
defaults: new { controller = "Pets", action = "GetById", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = "Pets", action = "GetById", area = (string)null, page = (string)null, }),
|
||||
order: 0),
|
||||
|
||||
// Attribute routed endpoint 2
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"api/Pets",
|
||||
defaults: new { controller = "Pets", action = "GetAll", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = "Pets", action = "GetAll", area = (string)null, page = (string)null, }),
|
||||
order: 0),
|
||||
|
||||
// Attribute routed endpoint 2
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"api/Pets/{id}",
|
||||
defaults: new { controller = "Pets", action = "Update", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = "Pets", action = "Update", area = (string)null, page = (string)null, }),
|
||||
order: 0),
|
||||
|
||||
// Attribute routed endpoint 4
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"api/Inventory/{searchTerm}/{page}",
|
||||
defaults: new { controller = "Inventory", action = "Search", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = "Inventory", action = "Search", area = (string)null, page = (string)null, }),
|
||||
order: 0),
|
||||
|
||||
// Conventional routed endpoint 1
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: null,
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }),
|
||||
order: 2000,
|
||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), }),
|
||||
|
||||
// Conventional routed endpoint 2
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: null,
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = "Home", action = "About", area = (string)null, page = (string)null, }),
|
||||
order: 2000,
|
||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), }),
|
||||
|
||||
// Conventional routed endpoint 3
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: null,
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = "Store", action = "Browse", area = (string)null, page = (string)null, }),
|
||||
order: 2000,
|
||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), }),
|
||||
|
||||
// Conventional routed link generation route 1
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: null,
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = RoutePattern.RequiredValueAny, action = RoutePattern.RequiredValueAny, area = (string)null, page = (string)null, }),
|
||||
order: 2000,
|
||||
metadata: new object[] { new SuppressMatchingMetadata(), }),
|
||||
|
||||
// Conventional routed endpoint 4 (with area)
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Admin/{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: new { area = "Admin", },
|
||||
parameterPolicies: new { controller = "Admin", },
|
||||
requiredValues: new { area = "Admin", controller = "Users", action = "Add", page = (string)null, }),
|
||||
order: 1000,
|
||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), }),
|
||||
|
||||
// Conventional routed endpoint 5 (with area)
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Admin/{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: new { area = "Admin", },
|
||||
parameterPolicies: new { controller = "Admin", },
|
||||
requiredValues: new { area = "Admin", controller = "Users", action = "Remove", page = (string)null, }),
|
||||
order: 1000,
|
||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), }),
|
||||
|
||||
// Conventional routed link generation route 2
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Admin/{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: new { area = "Admin", },
|
||||
parameterPolicies: new { area = "Admin", },
|
||||
requiredValues: new { controller = RoutePattern.RequiredValueAny, action = RoutePattern.RequiredValueAny, area = "Admin", page = (string)null, }),
|
||||
order: 1000,
|
||||
metadata: new object[] { new SuppressMatchingMetadata(), }),
|
||||
|
||||
// Conventional routed link generation route 3 - this doesn't match any actions.
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"api/{controller}/{id?}",
|
||||
defaults: new { },
|
||||
parameterPolicies: new { },
|
||||
requiredValues: new { controller = RoutePattern.RequiredValueAny, action = (string)null, area = (string)null, page = (string)null, }),
|
||||
order: 3000,
|
||||
metadata: new object[] { new SuppressMatchingMetadata(), new RouteNameMetadata("custom"), }),
|
||||
|
||||
// Conventional routed link generation route 3 - this doesn't match any actions.
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"api/Foo/{custom2}",
|
||||
defaults: new { },
|
||||
parameterPolicies: new { },
|
||||
requiredValues: new { controller = (string)null, action = (string)null, area = (string)null, page = (string)null, }),
|
||||
order: 3000,
|
||||
metadata: new object[] { new SuppressMatchingMetadata(), new RouteNameMetadata("custom2"), }),
|
||||
|
||||
// Razor Page 1 primary endpoint
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Pages",
|
||||
defaults: new { page = "/Pages/Index", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = (string)null, action = (string)null, area = (string)null, page = "/Pages/Index", }),
|
||||
order: 0),
|
||||
|
||||
// Razor Page 1 secondary endpoint
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Pages/Index",
|
||||
defaults: new { page = "/Pages/Index", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = (string)null, action = (string)null, area = (string)null, page = "/Pages/Index", }),
|
||||
order: 0,
|
||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), }),
|
||||
|
||||
// Razor Page 2 primary endpoint
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Pages/Help/{id?}",
|
||||
defaults: new { page = "/Pages/Help", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = (string)null, action = (string)null, area = (string)null, page = "/Pages/Help", }),
|
||||
order: 0),
|
||||
|
||||
// Razor Page 3 primary endpoint
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Pages/About/{id?}",
|
||||
defaults: new { page = "/Pages/About", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = (string)null, action = (string)null, area = (string)null, page = "/Pages/About", }),
|
||||
order: 0),
|
||||
|
||||
// Razor Page 4 with area primary endpoint
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Admin/Pages",
|
||||
defaults: new { page = "/Pages/Index", area = "Admin", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = (string)null, action = (string)null, area = "Admin", page = "/Pages/Index", }),
|
||||
order: 0),
|
||||
|
||||
// Razor Page 4 with area secondary endpoint
|
||||
EndpointFactory.CreateRouteEndpoint(
|
||||
RoutePatternFactory.Parse(
|
||||
"Admin/Pages/Index",
|
||||
defaults: new { page = "/Pages/Index", area = "Admin", },
|
||||
parameterPolicies: null,
|
||||
requiredValues: new { controller = (string)null, action = (string)null, area = "Admin", page = "/Pages/Index", }),
|
||||
order: 0,
|
||||
metadata: new object[] { new SuppressLinkGenerationMetadata(), }),
|
||||
};
|
||||
|
||||
Endpoints = endpoints;
|
||||
LinkGenerator = CreateLinkGenerator(endpoints.ToArray());
|
||||
}
|
||||
|
||||
private IReadOnlyList<Endpoint> Endpoints { get; }
|
||||
|
||||
private LinkGenerator LinkGenerator { get; }
|
||||
|
||||
#region Without ambient values (simple cases)
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToAttributedAction_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Pets", action = "GetById", id = "17", };
|
||||
var ambientValues = new { };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/api/Pets/17", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalAction_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Home", action = "Index", };
|
||||
var ambientValues = new { };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalActionInArea_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { area = "Admin", controller = "Users", action = "Add", };
|
||||
var ambientValues = new { };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Admin/Users/Add", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalRoute_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Store", id = "17", };
|
||||
var ambientValues = new { };
|
||||
var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/api/Store/17", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToPage_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { page = "/Pages/Index", };
|
||||
var ambientValues = new { };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Pages", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToPageInArea_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { area = "Admin", page = "/Pages/Index", };
|
||||
var ambientValues = new { };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Admin/Pages", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToNonExistentAction_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Home", action = "Fake", id = "17", };
|
||||
var ambientValues = new { };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Home/Fake/17", path);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region With ambient values
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToAttributedAction_FromSameAction_KeepsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Pets", action = "GetById", };
|
||||
var ambientValues = new { controller = "Pets", action = "GetById", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/api/Pets/17", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToAttributedAction_FromAnotherAction_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Pets", action = "GetById", };
|
||||
var ambientValues = new { controller = "Pets", action = "Update", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Pets/GetById", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToAttributedAction_FromPage_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Pets", action = "GetById", };
|
||||
var ambientValues = new { page = "/Pages/Help", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Pets/GetById", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalAction_FromSameAction_KeepsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Home", action = "Index", };
|
||||
var ambientValues = new { controller = "Home", action = "Index", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Home/Index/17", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalAction_FromAnotherAction_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Home", action = "Index", };
|
||||
var ambientValues = new { controller = "Pets", action = "Update", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalAction_FromPage_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Home", action = "Index", };
|
||||
var ambientValues = new { page = "/Pages/Help", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToNonExistentConventionalAction_FromAnotherAction_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Home", action = "Index11", };
|
||||
var ambientValues = new { controller = "Pets", action = "Update", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Home/Index11", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToNonExistentAreaAction_FromAnotherAction_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { area = "Admin", controller = "Home", action = "Index11", };
|
||||
var ambientValues = new { controller = "Pets", action = "Update", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Admin/Home/Index11", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalRoute_FromAction_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Store", };
|
||||
var ambientValues = new { controller = "Home", action = "Index", id = "17", };
|
||||
var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/api/Store", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalRoute_WithAmbientValues_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { controller = "Store", id = "17", };
|
||||
var ambientValues = new { controller = "Store", };
|
||||
var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/api/Store/17", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToConventionalRouteWithoutSharedAmbientValues_WithAmbientValues_GeneratesPath()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { custom2 = "17", };
|
||||
var ambientValues = new { controller = "Store", };
|
||||
var address = CreateAddress(routeName: "custom2", values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/api/Foo/17", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToPage_FromSamePage_KeepsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { page = "/Pages/Help", };
|
||||
var ambientValues = new { page = "/Pages/Help", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Pages/Help/17", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToPage_FromAction_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { page = "/Pages/Help", };
|
||||
var ambientValues = new { controller = "Pets", action = "Update", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Pages/Help", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToPage_FromAnotherPage_DiscardsAmbientValues()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { page = "/Pages/Help", };
|
||||
var ambientValues = new { page = "/Pages/About", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Pages/Help", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToNonExistentPage_FromAction_MatchesActionConventionalRoute()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { page = "/Pages/Help2", };
|
||||
var ambientValues = new { controller = "Pets", action = "Update", id = "17", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Pets/Update?page=%2FPages%2FHelp2", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPathByAddress_LinkToPageInSameArea_FromAction_UsingAreaAmbientValue()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateHttpContext();
|
||||
|
||||
var values = new { page = "/Pages/Index", };
|
||||
var ambientValues = new { area = "Admin", controller = "Users", action = "Add", };
|
||||
var address = CreateAddress(values: values, ambientValues: ambientValues);
|
||||
|
||||
// Act
|
||||
var path = LinkGenerator.GetPathByAddress(
|
||||
httpContext,
|
||||
address,
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/Admin/Pages", path);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static RouteValuesAddress CreateAddress(string routeName = null, object values = null, object ambientValues = null)
|
||||
{
|
||||
return new RouteValuesAddress()
|
||||
{
|
||||
RouteName = routeName,
|
||||
ExplicitValues = new RouteValueDictionary(values),
|
||||
AmbientValues = new RouteValueDictionary(ambientValues),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
|
|
@ -8,8 +9,6 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// 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.
|
||||
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
|
|
@ -62,6 +62,32 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubstituteRequiredValues_AllowRequiredValueAnyForParameter()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{controller=Home}/{action=Index}/{id?}";
|
||||
var defaults = new { };
|
||||
var policies = new { };
|
||||
|
||||
var original = RoutePatternFactory.Parse(template, defaults, policies);
|
||||
|
||||
var requiredValues = new { controller = RoutePattern.RequiredValueAny, };
|
||||
|
||||
// Act
|
||||
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
actual.Defaults.OrderBy(kvp => kvp.Key),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp)); // default is preserved
|
||||
|
||||
Assert.Collection(
|
||||
actual.RequiredValues.OrderBy(kvp => kvp.Key),
|
||||
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", RoutePattern.RequiredValueAny), kvp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubstituteRequiredValues_RejectsNullForOutOfLineDefault()
|
||||
{
|
||||
|
|
@ -81,6 +107,25 @@ namespace Microsoft.AspNetCore.Routing.Patterns
|
|||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubstituteRequiredValues_RejectsRequiredValueAnyForOutOfLineDefault()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{controller=Home}/{action=Index}/{id?}";
|
||||
var defaults = new { area = RoutePattern.RequiredValueAny };
|
||||
var policies = new { };
|
||||
|
||||
var original = RoutePatternFactory.Parse(template, defaults, policies);
|
||||
|
||||
var requiredValues = new { area = string.Empty, };
|
||||
|
||||
// Act
|
||||
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
|
||||
|
||||
// Assert
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubstituteRequiredValues_CanAcceptValueForParameter()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,12 +50,12 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureControllerServices(endpoints);
|
||||
|
||||
var dataSource = GetOrCreateDataSource(endpoints);
|
||||
dataSource.AddRoute(new ConventionalRouteEntry(
|
||||
dataSource.AddRoute(
|
||||
"default",
|
||||
"{controller=Home}/{action=Index}/{id?}",
|
||||
defaults: null,
|
||||
constraints: null,
|
||||
dataTokens: null));
|
||||
dataTokens: null);
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
|
|
@ -96,12 +96,12 @@ namespace Microsoft.AspNetCore.Builder
|
|||
EnsureControllerServices(endpoints);
|
||||
|
||||
var dataSource = GetOrCreateDataSource(endpoints);
|
||||
dataSource.AddRoute(new ConventionalRouteEntry(
|
||||
dataSource.AddRoute(
|
||||
name,
|
||||
pattern,
|
||||
new RouteValueDictionary(defaults),
|
||||
new RouteValueDictionary(constraints),
|
||||
new RouteValueDictionary(dataTokens)));
|
||||
new RouteValueDictionary(dataTokens));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -269,7 +269,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
// Endpoint Routing / Endpoints
|
||||
//
|
||||
services.TryAddSingleton<ActionEndpointDataSource>();
|
||||
services.TryAddSingleton<ControllerActionEndpointDataSource>();
|
||||
services.TryAddSingleton<ActionEndpointFactory>();
|
||||
services.TryAddSingleton<DynamicControllerEndpointSelector>();
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
internal class ActionEndpointDataSource : ActionEndpointDataSourceBase
|
||||
{
|
||||
private readonly ActionEndpointFactory _endpointFactory;
|
||||
private readonly List<ConventionalRouteEntry> _routes;
|
||||
|
||||
public ActionEndpointDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
|
||||
: base(actions)
|
||||
{
|
||||
_endpointFactory = endpointFactory;
|
||||
|
||||
_routes = new List<ConventionalRouteEntry>();
|
||||
|
||||
// IMPORTANT: this needs to be the last thing we do in the constructor.
|
||||
// Change notifications can happen immediately!
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
// For testing
|
||||
public IReadOnlyList<ConventionalRouteEntry> Routes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
return _routes.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRoute(in ConventionalRouteEntry route)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
_routes.Add(route);
|
||||
}
|
||||
}
|
||||
|
||||
protected override List<Endpoint> CreateEndpoints(IReadOnlyList<ActionDescriptor> actions, IReadOnlyList<Action<EndpointBuilder>> conventions)
|
||||
{
|
||||
var endpoints = new List<Endpoint>();
|
||||
for (var i = 0; i < actions.Count; i++)
|
||||
{
|
||||
_endpointFactory.AddEndpoints(endpoints, actions[i], _routes, conventions);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
|
|
@ -58,14 +59,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
if (action.AttributeRouteInfo == null)
|
||||
{
|
||||
// In traditional conventional routing setup, the routes defined by a user have a static order
|
||||
// defined by how they are added into the list. We would like to maintain the same order when building
|
||||
// up the endpoints too.
|
||||
//
|
||||
// Start with an order of '1' for conventional routes as attribute routes have a default order of '0'.
|
||||
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
|
||||
var conventionalRouteOrder = 1;
|
||||
|
||||
// Check each of the conventional patterns to see if the action would be reachable.
|
||||
// If the action and pattern are compatible then create an endpoint with action
|
||||
// route values on the pattern.
|
||||
|
|
@ -80,13 +73,15 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
continue;
|
||||
}
|
||||
|
||||
// We suppress link generation for each conventionally routed endpoint. We generate a single endpoint per-route
|
||||
// to handle link generation.
|
||||
var builder = CreateEndpoint(
|
||||
action,
|
||||
updatedRoutePattern,
|
||||
route.RouteName,
|
||||
conventionalRouteOrder++,
|
||||
route.Order,
|
||||
route.DataTokens,
|
||||
suppressLinkGeneration: false,
|
||||
suppressLinkGeneration: true,
|
||||
suppressPathMatching: false,
|
||||
conventions);
|
||||
endpoints.Add(builder);
|
||||
|
|
@ -119,6 +114,72 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
}
|
||||
}
|
||||
|
||||
public void AddConventionalLinkGenerationRoute(List<Endpoint> endpoints, HashSet<string> keys, ConventionalRouteEntry route, IReadOnlyList<Action<EndpointBuilder>> conventions)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (keys == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(keys));
|
||||
}
|
||||
|
||||
if (conventions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(conventions));
|
||||
}
|
||||
|
||||
var requiredValues = new RouteValueDictionary();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (route.Pattern.GetParameter(key) != null)
|
||||
{
|
||||
// Parameter (allow any)
|
||||
requiredValues[key] = RoutePattern.RequiredValueAny;
|
||||
}
|
||||
else if (route.Pattern.Defaults.TryGetValue(key, out var value))
|
||||
{
|
||||
requiredValues[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
requiredValues[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// We have to do some massaging of the pattern to try and get the
|
||||
// required values to be correct.
|
||||
var pattern = _routePatternTransformer.SubstituteRequiredValues(route.Pattern, requiredValues);
|
||||
if (pattern == null)
|
||||
{
|
||||
// We don't expect this to happen, but we want to know if it does because it will help diagnose the bug.
|
||||
throw new InvalidOperationException("Failed to create a conventional route for pattern: " + route.Pattern);
|
||||
}
|
||||
|
||||
var builder = new RouteEndpointBuilder(context => Task.CompletedTask, pattern, route.Order)
|
||||
{
|
||||
DisplayName = "Route: " + route.Pattern.RawText,
|
||||
Metadata =
|
||||
{
|
||||
new SuppressMatchingMetadata(),
|
||||
},
|
||||
};
|
||||
|
||||
if (route.RouteName != null)
|
||||
{
|
||||
builder.Metadata.Add(new RouteNameMetadata(route.RouteName));
|
||||
}
|
||||
|
||||
for (var i = 0; i < conventions.Count; i++)
|
||||
{
|
||||
conventions[i](builder);
|
||||
}
|
||||
|
||||
endpoints.Add((RouteEndpoint)builder.Build());
|
||||
}
|
||||
|
||||
private static (RoutePattern resolvedRoutePattern, IDictionary<string, string> resolvedRequiredValues) ResolveDefaultsAndRequiredValues(ActionDescriptor action, RoutePattern attributeRoutePattern)
|
||||
{
|
||||
RouteValueDictionary updatedDefaults = null;
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
|
|
@ -16,37 +19,81 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
private readonly ActionEndpointFactory _endpointFactory;
|
||||
private readonly List<ConventionalRouteEntry> _routes;
|
||||
|
||||
public ControllerActionEndpointDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
|
||||
private int _order;
|
||||
|
||||
public ControllerActionEndpointDataSource(
|
||||
IActionDescriptorCollectionProvider actions,
|
||||
ActionEndpointFactory endpointFactory)
|
||||
: base(actions)
|
||||
{
|
||||
_endpointFactory = endpointFactory;
|
||||
|
||||
_routes = new List<ConventionalRouteEntry>();
|
||||
|
||||
// In traditional conventional routing setup, the routes defined by a user have a order
|
||||
// defined by how they are added into the list. We would like to maintain the same order when building
|
||||
// up the endpoints too.
|
||||
//
|
||||
// Start with an order of '1' for conventional routes as attribute routes have a default order of '0'.
|
||||
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
|
||||
_order = 1;
|
||||
|
||||
// IMPORTANT: this needs to be the last thing we do in the constructor.
|
||||
// Change notifications can happen immediately!
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
public void AddRoute(in ConventionalRouteEntry route)
|
||||
|
||||
public void AddRoute(
|
||||
string routeName,
|
||||
string pattern,
|
||||
RouteValueDictionary defaults,
|
||||
IDictionary<string, object> constraints,
|
||||
RouteValueDictionary dataTokens)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
_routes.Add(route);
|
||||
_routes.Add(new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, _order++));
|
||||
}
|
||||
}
|
||||
|
||||
protected override List<Endpoint> CreateEndpoints(IReadOnlyList<ActionDescriptor> actions, IReadOnlyList<Action<EndpointBuilder>> conventions)
|
||||
{
|
||||
var endpoints = new List<Endpoint>();
|
||||
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// For each controller action - add the relevant endpoints.
|
||||
//
|
||||
// 1. If the action is attribute routed, we use that information verbatim
|
||||
// 2. If the action is conventional routed
|
||||
// a. Create a *matching only* endpoint for each action X route (if possible)
|
||||
// b. Ignore link generation for now
|
||||
for (var i = 0; i < actions.Count; i++)
|
||||
{
|
||||
if (actions[i] is ControllerActionDescriptor action)
|
||||
{
|
||||
_endpointFactory.AddEndpoints(endpoints, action, _routes, conventions);
|
||||
|
||||
if (_routes.Count > 0)
|
||||
{
|
||||
// If we have conventional routes, keep track of the keys so we can create
|
||||
// the link generation routes later.
|
||||
foreach (var kvp in action.RouteValues)
|
||||
{
|
||||
keys.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now create a *link generation only* endpoint for each route. This gives us a very
|
||||
// compatible experience to previous versions.
|
||||
for (var i = 0; i < _routes.Count; i++)
|
||||
{
|
||||
var route = _routes[i];
|
||||
_endpointFactory.AddConventionalLinkGenerationRoute(endpoints, keys, route, conventions);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,19 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
public readonly RoutePattern Pattern;
|
||||
public readonly string RouteName;
|
||||
public readonly RouteValueDictionary DataTokens;
|
||||
public readonly int Order;
|
||||
|
||||
public ConventionalRouteEntry(
|
||||
string routeName,
|
||||
string pattern,
|
||||
RouteValueDictionary defaults,
|
||||
IDictionary<string, object> constraints,
|
||||
RouteValueDictionary dataTokens)
|
||||
RouteValueDictionary dataTokens,
|
||||
int order)
|
||||
{
|
||||
RouteName = routeName;
|
||||
DataTokens = dataTokens;
|
||||
Order = order;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -40,17 +43,5 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
pattern), exception);
|
||||
}
|
||||
}
|
||||
|
||||
public ConventionalRouteEntry(RoutePattern pattern, string routeName, RouteValueDictionary dataTokens)
|
||||
{
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
Pattern = pattern;
|
||||
RouteName = routeName;
|
||||
DataTokens = dataTokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||
{
|
||||
public class ActionEndpointDataSourceTest : ActionEndpointDataSourceBaseTest
|
||||
{
|
||||
[Fact]
|
||||
public void Endpoints_MultipledActions_MultipleRoutes()
|
||||
{
|
||||
// Arrange
|
||||
var actions = new List<ActionDescriptor>
|
||||
{
|
||||
new ActionDescriptor
|
||||
{
|
||||
AttributeRouteInfo = new AttributeRouteInfo()
|
||||
{
|
||||
Template = "/test",
|
||||
},
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "action", "Test" },
|
||||
{ "controller", "Test" },
|
||||
},
|
||||
},
|
||||
new ActionDescriptor
|
||||
{
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "action", "Index" },
|
||||
{ "controller", "Home" },
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
|
||||
mockDescriptorProvider
|
||||
.Setup(m => m.ActionDescriptors)
|
||||
.Returns(new ActionDescriptorCollection(actions, 0));
|
||||
|
||||
var dataSource = (ActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null));
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null));
|
||||
|
||||
// Act
|
||||
var endpoints = dataSource.Endpoints;
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
endpoints.Cast<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/test", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Endpoints_AppliesConventions()
|
||||
{
|
||||
// Arrange
|
||||
var actions = new List<ActionDescriptor>
|
||||
{
|
||||
new ActionDescriptor
|
||||
{
|
||||
AttributeRouteInfo = new AttributeRouteInfo()
|
||||
{
|
||||
Template = "/test",
|
||||
},
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "action", "Test" },
|
||||
{ "controller", "Test" },
|
||||
},
|
||||
},
|
||||
new ActionDescriptor
|
||||
{
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "action", "Index" },
|
||||
{ "controller", "Home" },
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
|
||||
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
|
||||
|
||||
var dataSource = (ActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null));
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null));
|
||||
|
||||
dataSource.Add((b) =>
|
||||
{
|
||||
b.Metadata.Add("Hi there");
|
||||
});
|
||||
|
||||
// Act
|
||||
var endpoints = dataSource.Endpoints;
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
endpoints.OfType<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/test", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
|
||||
});
|
||||
}
|
||||
|
||||
private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
|
||||
{
|
||||
return new ActionEndpointDataSource(actions, endpointFactory);
|
||||
}
|
||||
|
||||
protected override ActionDescriptor CreateActionDescriptor(
|
||||
object values,
|
||||
string pattern = null,
|
||||
IList<object> metadata = null)
|
||||
{
|
||||
var action = new ActionDescriptor();
|
||||
|
||||
foreach (var kvp in new RouteValueDictionary(values))
|
||||
{
|
||||
action.RouteValues[kvp.Key] = kvp.Value?.ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(pattern))
|
||||
{
|
||||
action.AttributeRouteInfo = new AttributeRouteInfo
|
||||
{
|
||||
Name = "test",
|
||||
Template = pattern,
|
||||
};
|
||||
}
|
||||
|
||||
action.EndpointMetadata = metadata;
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -269,8 +269,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var action = CreateActionDescriptor(values);
|
||||
var routes = new[]
|
||||
{
|
||||
CreateRoute(routeName: "test1", pattern: "{controller}/{action}/{id?}"),
|
||||
CreateRoute(routeName: "test2", pattern: "named/{controller}/{action}/{id?}"),
|
||||
CreateRoute(routeName: "test1", pattern: "{controller}/{action}/{id?}", order: 1),
|
||||
CreateRoute(routeName: "test2", pattern: "named/{controller}/{action}/{id?}", order: 2),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -306,7 +306,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, string template)
|
||||
{
|
||||
return CreateConventionalRoutedEndpoint(action, new ConventionalRouteEntry(routeName: null, template, null, null, null));
|
||||
return CreateConventionalRoutedEndpoint(action, new ConventionalRouteEntry(routeName: null, template, null, null, null, order: 0));
|
||||
}
|
||||
|
||||
private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, ConventionalRouteEntry route)
|
||||
|
|
@ -340,9 +340,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
string pattern,
|
||||
RouteValueDictionary defaults = null,
|
||||
IDictionary<string, object> constraints = null,
|
||||
RouteValueDictionary dataTokens = null)
|
||||
RouteValueDictionary dataTokens = null,
|
||||
int order = 0)
|
||||
{
|
||||
return new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens);
|
||||
return new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, order);
|
||||
}
|
||||
|
||||
private ActionDescriptor CreateActionDescriptor(
|
||||
|
|
|
|||
|
|
@ -81,15 +81,15 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
.Returns(new ActionDescriptorCollection(actions, 0));
|
||||
|
||||
var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null));
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null));
|
||||
dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
|
||||
dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
|
||||
|
||||
// Act
|
||||
var endpoints = dataSource.Endpoints;
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
endpoints.Cast<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText),
|
||||
endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
|
|
@ -99,6 +99,19 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
{
|
||||
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
});
|
||||
|
||||
Assert.Collection(
|
||||
endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
|
|
@ -139,8 +152,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
|
||||
|
||||
var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("1", "/1/{controller}/{action}/{id?}", null, null, null));
|
||||
dataSource.AddRoute(new ConventionalRouteEntry("2", "/2/{controller}/{action}/{id?}", null, null, null));
|
||||
dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
|
||||
dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
|
||||
|
||||
dataSource.Add((b) =>
|
||||
{
|
||||
|
|
@ -152,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
endpoints.OfType<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText),
|
||||
endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
|
|
@ -164,6 +177,21 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
|
||||
});
|
||||
|
||||
Assert.Collection(
|
||||
endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
|
||||
Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
|
||||
Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
|
|
@ -173,6 +201,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
});
|
||||
}
|
||||
|
||||
private static bool SupportsLinkGeneration(RouteEndpoint endpoint)
|
||||
{
|
||||
return !(endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true);
|
||||
}
|
||||
|
||||
private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
|
||||
{
|
||||
return new ControllerActionEndpointDataSource(actions, endpointFactory);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Routing.Patterns;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc.Performance
|
||||
{
|
||||
public class ActionEndpointDataSourceBenchmark
|
||||
public class ControllerActionEndpointDataSourceBenchmark
|
||||
{
|
||||
private const string DefaultRoute = "{Controller=Home}/{Action=Index}/{id?}";
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
|||
|
||||
private MockActionDescriptorCollectionProvider _conventionalActionProvider;
|
||||
private MockActionDescriptorCollectionProvider _attributeActionProvider;
|
||||
private List<ConventionalRouteEntry> _routes;
|
||||
private List<(string routeName, string pattern)> _routes;
|
||||
|
||||
[Params(1, 100, 1000)]
|
||||
public int ActionCount;
|
||||
|
|
@ -41,14 +41,9 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
|||
Enumerable.Range(0, ActionCount).Select(i => CreateAttributeRoutedAction(i)).ToList()
|
||||
);
|
||||
|
||||
_routes = new List<ConventionalRouteEntry>
|
||||
_routes = new List<(string routeName, string pattern)>
|
||||
{
|
||||
new ConventionalRouteEntry(
|
||||
"Default",
|
||||
DefaultRoute,
|
||||
new RouteValueDictionary(),
|
||||
new Dictionary<string, object>(),
|
||||
new RouteValueDictionary())
|
||||
("Default", DefaultRoute)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +62,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
|||
var dataSource = CreateDataSource(_conventionalActionProvider);
|
||||
for (var i = 0; i < _routes.Count; i++)
|
||||
{
|
||||
dataSource.AddRoute(_routes[i]);
|
||||
var (routeName, pattern) = _routes[i];
|
||||
dataSource.AddRoute(routeName, pattern, defaults: null, constraints: null, dataTokens: null);
|
||||
}
|
||||
|
||||
var endpoints = dataSource.Endpoints;
|
||||
|
|
@ -110,9 +106,9 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
|||
};
|
||||
}
|
||||
|
||||
private ActionEndpointDataSource CreateDataSource(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
|
||||
private ControllerActionEndpointDataSource CreateDataSource(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
|
||||
{
|
||||
var dataSource = new ActionEndpointDataSource(
|
||||
var dataSource = new ControllerActionEndpointDataSource(
|
||||
actionDescriptorCollectionProvider,
|
||||
new ActionEndpointFactory(new MockRoutePatternTransformer()));
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("/Admin/LG3/SomeAction", responseContent);
|
||||
}
|
||||
|
||||
// Rejected because the calling code relies on ambient values, but doesn't pass
|
||||
// This will fallback to the non-area route because the calling code relies on ambient values, but doesn't pass
|
||||
// the HttpContext.
|
||||
[Fact]
|
||||
public async Task GetPathByAction_FailsToGenerateLinkInsideArea()
|
||||
|
|
@ -106,8 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
Assert.Equal(string.Empty, responseContent);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("/LG3/SomeAction", responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -118,8 +118,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
Assert.Equal(string.Empty, responseContent);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("/LG1/SomeAction", responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -229,5 +229,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("https://www.example.com/Admin/LGAreaPage?handler=a-handler", responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUriByRouteValues_CanGenerateUriToRouteWithoutMvcParameters()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync("LG1/LinkToRouteWithNoMvcParameters?custom=17");
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("https://www.example.com/routewithnomvcparameters/17", responseContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,48 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.False(result);
|
||||
}
|
||||
|
||||
// Legacy routing supports linking to actions that don't exist
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_InArea_StaysInArea_ActionDoesntExist()
|
||||
{
|
||||
// Arrange
|
||||
var url = LinkFrom("http://localhost/ContosoCorp/Trains")
|
||||
.To(new { action = "Contact", controller = "Home", });
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Rail", result.Controller);
|
||||
Assert.Equal("Index", result.Action);
|
||||
|
||||
Assert.Equal("/Travel/Home/Contact", result.Link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConventionalRoutedAction_InArea_StaysInArea()
|
||||
{
|
||||
// Arrange
|
||||
var url = LinkFrom("http://localhost/Travel/Flight").To(new { action = "Contact", controller = "Home", });
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Flight", result.Controller);
|
||||
Assert.Equal("Index", result.Action);
|
||||
|
||||
Assert.Equal("/Travel/Home/Contact", result.Link);
|
||||
}
|
||||
|
||||
// Legacy routing returns 404 when an action does not support a HTTP method.
|
||||
[Fact]
|
||||
public override async Task AttributeRoutedAction_MultipleRouteAttributes_RouteAttributeTemplatesIgnoredForOverrideActions()
|
||||
|
|
|
|||
|
|
@ -93,6 +93,47 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.False(result.RouteValues.ContainsKey("page"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_InArea_StaysInArea_ActionDoesntExist()
|
||||
{
|
||||
// Arrange
|
||||
var url = LinkFrom("http://localhost/ContosoCorp/Trains")
|
||||
.To(new { action = "Contact", controller = "Home", });
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Rail", result.Controller);
|
||||
Assert.Equal("Index", result.Action);
|
||||
|
||||
Assert.Equal("/Travel/Home/Contact", result.Link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConventionalRoutedAction_InArea_StaysInArea()
|
||||
{
|
||||
// Arrange
|
||||
var url = LinkFrom("http://localhost/Travel/Flight").To(new { action = "Contact", controller = "Home", });
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Flight", result.Controller);
|
||||
Assert.Equal("Index", result.Action);
|
||||
|
||||
Assert.Equal("/Travel/Home/Contact", result.Link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public abstract Task HasEndpointMatch();
|
||||
|
||||
|
|
|
|||
|
|
@ -45,26 +45,26 @@
|
|||
<a href="/Order/List">Href Order List</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Non-existent Controller</a>
|
||||
<a href="HtmlEncode[[/NonExistentController]]">Non-existent Controller</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">
|
||||
<a href="HtmlEncode[[/NonExistentController#fragment]]">
|
||||
Non-existent Controller Fragment
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Non-existent Action</a>
|
||||
<a href="HtmlEncode[[/Order/NonExistentAction]]">Non-existent Action</a>
|
||||
</div>
|
||||
<div>
|
||||
<a id="Id" href="HtmlEncode[[http://somewhere/]]">Some Where</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">
|
||||
<a href="HtmlEncode[[unknown://localhost/NoControll#fragment]]">
|
||||
Unknown Protocol Non-existent Controller Fragment
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Product Route Non-existent Area Parameter</a>
|
||||
<a href="HtmlEncode[[/Product/Submit?area=NonExistentArea&id=1#fragment]]">Product Route Non-existent Area Parameter</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Non-existent Area</a>
|
||||
|
|
|
|||
|
|
@ -45,26 +45,26 @@
|
|||
<a href="/Order/List">Href Order List</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Non-existent Controller</a>
|
||||
<a href="/NonExistentController">Non-existent Controller</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">
|
||||
<a href="/NonExistentController#fragment">
|
||||
Non-existent Controller Fragment
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Non-existent Action</a>
|
||||
<a href="/Order/NonExistentAction">Non-existent Action</a>
|
||||
</div>
|
||||
<div>
|
||||
<a id="Id" href="http://somewhere/">Some Where</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">
|
||||
<a href="unknown://localhost/NoControll#fragment">
|
||||
Unknown Protocol Non-existent Controller Fragment
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Product Route Non-existent Area Parameter</a>
|
||||
<a href="/Product/Submit?area=NonExistentArea&id=1#fragment">Product Route Non-existent Area Parameter</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="">Non-existent Area</a>
|
||||
|
|
|
|||
|
|
@ -119,6 +119,15 @@ namespace RoutingWebSite
|
|||
values: values);
|
||||
}
|
||||
|
||||
public string LinkToRouteWithNoMvcParameters(int? custom = null)
|
||||
{
|
||||
return _linkGenerator.GetUriByRouteValues(
|
||||
scheme: "https",
|
||||
host: new HostString("www.example.com"),
|
||||
routeName: "routewithnomvcparameters",
|
||||
values: new { custom = custom, });
|
||||
}
|
||||
|
||||
private static RouteValueDictionary QueryToRouteValues(IQueryCollection query)
|
||||
{
|
||||
return new RouteValueDictionary(query.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString()));
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ namespace RoutingWebSite
|
|||
{
|
||||
endpoints.MapDefaultControllerRoute();
|
||||
endpoints.MapRazorPages();
|
||||
|
||||
endpoints.MapControllerRoute("routewithnomvcparameters", "/routewithnomvcparameters/{custom}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue