Generalize Action Selection logic
Allows us to use the "select action via route values" logic for endpoints.
This commit is contained in:
parent
8a46e8cd93
commit
bb28db6fb2
|
|
@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Http;
|
|||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
// FYI: This class is also linked into MVC. If you make changes to the API you will
|
||||
// also need to change MVC's usage.
|
||||
internal sealed class DataSourceDependentCache<T> : IDisposable where T : class
|
||||
{
|
||||
private readonly EndpointDataSource _dataSource;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
// Common infrastructure for things that look up actions by route values.
|
||||
//
|
||||
// The ActionSelectionTable stores a mapping of route-values -> items for each known set of
|
||||
// of route-values. We actually build two of these mappings, one for case-sensitive (fast path) and one for
|
||||
// case-insensitive (slow path).
|
||||
//
|
||||
// This is necessary because MVC routing/action-selection is always case-insensitive. So we're going to build
|
||||
// a case-sensitive dictionary that will behave like the a case-insensitive dictionary when you hit one of the
|
||||
// canonical entries. When you don't hit a case-sensitive match it will try the case-insensitive dictionary
|
||||
// so you still get correct behaviors.
|
||||
//
|
||||
// The difference here is because while MVC is case-insensitive, doing a case-sensitive comparison is much
|
||||
// faster. We also expect that most of the URLs we process are canonically-cased because they were generated
|
||||
// by Url.Action or another routing api.
|
||||
//
|
||||
// This means that for a set of actions like:
|
||||
// { controller = "Home", action = "Index" } -> HomeController::Index1()
|
||||
// { controller = "Home", action = "index" } -> HomeController::Index2()
|
||||
//
|
||||
// Both of these actions match "Index" case-insensitively, but there exist two known canonical casings,
|
||||
// so we will create an entry for "Index" and an entry for "index". Both of these entries match **both**
|
||||
// actions.
|
||||
internal class ActionSelectionTable<TItem>
|
||||
{
|
||||
private ActionSelectionTable(
|
||||
int version,
|
||||
string[] routeKeys,
|
||||
Dictionary<string[], List<TItem>> ordinalEntries,
|
||||
Dictionary<string[], List<TItem>> ordinalIgnoreCaseEntries)
|
||||
{
|
||||
Version = version;
|
||||
RouteKeys = routeKeys;
|
||||
OrdinalEntries = ordinalEntries;
|
||||
OrdinalIgnoreCaseEntries = ordinalIgnoreCaseEntries;
|
||||
}
|
||||
|
||||
public int Version { get; }
|
||||
|
||||
private string[] RouteKeys { get; }
|
||||
|
||||
private Dictionary<string[], List<TItem>> OrdinalEntries { get; }
|
||||
|
||||
private Dictionary<string[], List<TItem>> OrdinalIgnoreCaseEntries { get; }
|
||||
|
||||
public static ActionSelectionTable<ActionDescriptor> Create(ActionDescriptorCollection actions)
|
||||
{
|
||||
return CreateCore<ActionDescriptor>(
|
||||
|
||||
// We need to store the version so the cache can be invalidated if the actions change.
|
||||
version: actions.Version,
|
||||
|
||||
// For action selection, ignore attribute routed actions
|
||||
items: actions.Items.Where(a => a.AttributeRouteInfo == null),
|
||||
|
||||
getRouteKeys: a => a.RouteValues.Keys,
|
||||
getRouteValue: (a, key) =>
|
||||
{
|
||||
a.RouteValues.TryGetValue(key, out var value);
|
||||
return value ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
public static ActionSelectionTable<RouteEndpoint> Create(IEnumerable<Endpoint> endpoints)
|
||||
{
|
||||
return CreateCore<RouteEndpoint>(
|
||||
|
||||
// we don't use version for endpoints
|
||||
version: 0,
|
||||
|
||||
// Only include RouteEndpoints and only those that aren't suppressed.
|
||||
items: endpoints.OfType<RouteEndpoint>().Where(e =>
|
||||
{
|
||||
return e.Metadata.GetMetadata<ISuppressMatchingMetadata>().SuppressMatching != true;
|
||||
}),
|
||||
|
||||
getRouteKeys: e => e.RoutePattern.RequiredValues.Keys,
|
||||
getRouteValue: (e, key) =>
|
||||
{
|
||||
e.RoutePattern.RequiredValues.TryGetValue(key, out var value);
|
||||
return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<T> CreateCore<T>(
|
||||
int version,
|
||||
IEnumerable<T> items,
|
||||
Func<T, IEnumerable<string>> getRouteKeys,
|
||||
Func<T, string, string> getRouteValue)
|
||||
{
|
||||
// We need to build two maps for all of the route values.
|
||||
var ordinalEntries = new Dictionary<string[], List<T>>(StringArrayComparer.Ordinal);
|
||||
var ordinalIgnoreCaseEntries = new Dictionary<string[], List<T>>(StringArrayComparer.OrdinalIgnoreCase);
|
||||
|
||||
// We need to hold on to an ordered set of keys for the route values. We'll use these later to
|
||||
// extract the set of route values from an incoming request to compare against our maps of known
|
||||
// route values.
|
||||
var routeKeys = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
foreach (var key in getRouteKeys(item))
|
||||
{
|
||||
routeKeys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
// This is a conventionally routed action - so we need to extract the route values associated
|
||||
// with this action (in order) so we can store them in our dictionaries.
|
||||
var index = 0;
|
||||
var routeValues = new string[routeKeys.Count];
|
||||
foreach (var key in routeKeys)
|
||||
{
|
||||
var value = getRouteValue(item, key);
|
||||
routeValues[index++] = value;
|
||||
}
|
||||
|
||||
if (!ordinalIgnoreCaseEntries.TryGetValue(routeValues, out var entries))
|
||||
{
|
||||
entries = new List<T>();
|
||||
ordinalIgnoreCaseEntries.Add(routeValues, entries);
|
||||
}
|
||||
|
||||
entries.Add(item);
|
||||
|
||||
// We also want to add the same (as in reference equality) list of actions to the ordinal entries.
|
||||
// We'll keep updating `entries` to include all of the actions in the same equivalence class -
|
||||
// meaning, all conventionally routed actions for which the route values are equal ignoring case.
|
||||
//
|
||||
// `entries` will appear in `OrdinalIgnoreCaseEntries` exactly once and in `OrdinalEntries` once
|
||||
// for each variation of casing that we've seen.
|
||||
if (!ordinalEntries.ContainsKey(routeValues))
|
||||
{
|
||||
ordinalEntries.Add(routeValues, entries);
|
||||
}
|
||||
}
|
||||
|
||||
return new ActionSelectionTable<T>(version, routeKeys.ToArray(), ordinalEntries, ordinalIgnoreCaseEntries);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TItem> Select(RouteValueDictionary values)
|
||||
{
|
||||
// Select works based on a string[] of the route values in a pre-calculated order. This code extracts
|
||||
// those values in the correct order.
|
||||
var routeKeys = RouteKeys;
|
||||
var routeValues = new string[routeKeys.Length];
|
||||
for (var i = 0; i < routeKeys.Length; i++)
|
||||
{
|
||||
values.TryGetValue(routeKeys[i], out var value);
|
||||
routeValues[i] = value as string ?? Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
|
||||
// Now look up, first case-sensitive, then case-insensitive.
|
||||
if (OrdinalEntries.TryGetValue(routeValues, out var matches) ||
|
||||
OrdinalIgnoreCaseEntries.TryGetValue(routeValues, out matches))
|
||||
{
|
||||
Debug.Assert(matches != null);
|
||||
Debug.Assert(matches.Count >= 0);
|
||||
return matches;
|
||||
}
|
||||
|
||||
return Array.Empty<TItem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,11 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
|
||||
|
||||
|
|
@ -22,13 +18,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
/// </summary>
|
||||
internal class ActionSelector : IActionSelector
|
||||
{
|
||||
private static readonly IReadOnlyList<ActionDescriptor> EmptyActions = Array.Empty<ActionDescriptor>();
|
||||
|
||||
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
||||
private readonly ActionConstraintCache _actionConstraintCache;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private Cache _cache;
|
||||
private ActionSelectionTable<ActionDescriptor> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ActionSelector"/>.
|
||||
|
|
@ -49,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
_actionConstraintCache = actionConstraintCache;
|
||||
}
|
||||
|
||||
private Cache Current
|
||||
private ActionSelectionTable<ActionDescriptor> Current
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -61,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return cache;
|
||||
}
|
||||
|
||||
cache = new Cache(actions);
|
||||
cache = ActionSelectionTable<ActionDescriptor>.Create(actions);
|
||||
Volatile.Write(ref _cache, cache);
|
||||
return cache;
|
||||
}
|
||||
|
|
@ -76,28 +70,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
|
||||
var cache = Current;
|
||||
|
||||
// The Cache works based on a string[] of the route values in a pre-calculated order. This code extracts
|
||||
// those values in the correct order.
|
||||
var keys = cache.RouteKeys;
|
||||
var values = new string[keys.Length];
|
||||
for (var i = 0; i < keys.Length; i++)
|
||||
var matches = cache.Select(context.RouteData.Values);
|
||||
if (matches.Count > 0)
|
||||
{
|
||||
context.RouteData.Values.TryGetValue(keys[i], out var value);
|
||||
if (value != null)
|
||||
{
|
||||
values[i] = value as string ?? Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
if (cache.OrdinalEntries.TryGetValue(values, out var matchingRouteValues) ||
|
||||
cache.OrdinalIgnoreCaseEntries.TryGetValue(values, out matchingRouteValues))
|
||||
{
|
||||
Debug.Assert(matchingRouteValues != null);
|
||||
return matchingRouteValues;
|
||||
return matches;
|
||||
}
|
||||
|
||||
_logger.NoActionsMatched(context.RouteData.Values);
|
||||
return EmptyActions;
|
||||
return matches;
|
||||
}
|
||||
|
||||
public ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
|
||||
|
|
@ -286,175 +266,5 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return EvaluateActionConstraintsCore(context, actionsWithoutConstraint, order);
|
||||
}
|
||||
}
|
||||
|
||||
// The action selector cache stores a mapping of route-values -> action descriptors for each known set of
|
||||
// of route-values. We actually build two of these mappings, one for case-sensitive (fast path) and one for
|
||||
// case-insensitive (slow path).
|
||||
//
|
||||
// This is necessary because MVC routing/action-selection is always case-insensitive. So we're going to build
|
||||
// a case-sensitive dictionary that will behave like the a case-insensitive dictionary when you hit one of the
|
||||
// canonical entries. When you don't hit a case-sensitive match it will try the case-insensitive dictionary
|
||||
// so you still get correct behaviors.
|
||||
//
|
||||
// The difference here is because while MVC is case-insensitive, doing a case-sensitive comparison is much
|
||||
// faster. We also expect that most of the URLs we process are canonically-cased because they were generated
|
||||
// by Url.Action or another routing api.
|
||||
//
|
||||
// This means that for a set of actions like:
|
||||
// { controller = "Home", action = "Index" } -> HomeController::Index1()
|
||||
// { controller = "Home", action = "index" } -> HomeController::Index2()
|
||||
//
|
||||
// Both of these actions match "Index" case-insensitively, but there exist two known canonical casings,
|
||||
// so we will create an entry for "Index" and an entry for "index". Both of these entries match **both**
|
||||
// actions.
|
||||
private class Cache
|
||||
{
|
||||
public Cache(ActionDescriptorCollection actions)
|
||||
{
|
||||
// We need to store the version so the cache can be invalidated if the actions change.
|
||||
Version = actions.Version;
|
||||
|
||||
// We need to build two maps for all of the route values.
|
||||
OrdinalEntries = new Dictionary<string[], List<ActionDescriptor>>(StringArrayComparer.Ordinal);
|
||||
OrdinalIgnoreCaseEntries = new Dictionary<string[], List<ActionDescriptor>>(StringArrayComparer.OrdinalIgnoreCase);
|
||||
|
||||
// We need to first identify of the keys that action selection will look at (in route data).
|
||||
// We want to only consider conventionally routed actions here.
|
||||
var routeKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var i = 0; i < actions.Items.Count; i++)
|
||||
{
|
||||
var action = actions.Items[i];
|
||||
if (action.AttributeRouteInfo == null)
|
||||
{
|
||||
// This is a conventionally routed action - so make sure we include its keys in the set of
|
||||
// known route value keys.
|
||||
foreach (var kvp in action.RouteValues)
|
||||
{
|
||||
routeKeys.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to hold on to an ordered set of keys for the route values. We'll use these later to
|
||||
// extract the set of route values from an incoming request to compare against our maps of known
|
||||
// route values.
|
||||
RouteKeys = routeKeys.ToArray();
|
||||
|
||||
for (var i = 0; i < actions.Items.Count; i++)
|
||||
{
|
||||
var action = actions.Items[i];
|
||||
if (action.AttributeRouteInfo != null)
|
||||
{
|
||||
// This only handles conventional routing. Ignore attribute routed actions.
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is a conventionally routed action - so we need to extract the route values associated
|
||||
// with this action (in order) so we can store them in our dictionaries.
|
||||
var routeValues = new string[RouteKeys.Length];
|
||||
for (var j = 0; j < RouteKeys.Length; j++)
|
||||
{
|
||||
action.RouteValues.TryGetValue(RouteKeys[j], out routeValues[j]);
|
||||
}
|
||||
|
||||
if (!OrdinalIgnoreCaseEntries.TryGetValue(routeValues, out var entries))
|
||||
{
|
||||
entries = new List<ActionDescriptor>();
|
||||
OrdinalIgnoreCaseEntries.Add(routeValues, entries);
|
||||
}
|
||||
|
||||
entries.Add(action);
|
||||
|
||||
// We also want to add the same (as in reference equality) list of actions to the ordinal entries.
|
||||
// We'll keep updating `entries` to include all of the actions in the same equivalence class -
|
||||
// meaning, all conventionally routed actions for which the route values are equal ignoring case.
|
||||
//
|
||||
// `entries` will appear in `OrdinalIgnoreCaseEntries` exactly once and in `OrdinalEntries` once
|
||||
// for each variation of casing that we've seen.
|
||||
if (!OrdinalEntries.ContainsKey(routeValues))
|
||||
{
|
||||
OrdinalEntries.Add(routeValues, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Version { get; }
|
||||
|
||||
public string[] RouteKeys { get; }
|
||||
|
||||
public Dictionary<string[], List<ActionDescriptor>> OrdinalEntries { get; }
|
||||
|
||||
public Dictionary<string[], List<ActionDescriptor>> OrdinalIgnoreCaseEntries { get; }
|
||||
}
|
||||
|
||||
private class StringArrayComparer : IEqualityComparer<string[]>
|
||||
{
|
||||
public static readonly StringArrayComparer Ordinal = new StringArrayComparer(StringComparer.Ordinal);
|
||||
|
||||
public static readonly StringArrayComparer OrdinalIgnoreCase = new StringArrayComparer(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly StringComparer _valueComparer;
|
||||
|
||||
private StringArrayComparer(StringComparer valueComparer)
|
||||
{
|
||||
_valueComparer = valueComparer;
|
||||
}
|
||||
|
||||
public bool Equals(string[] x, string[] y)
|
||||
{
|
||||
if (object.ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x == null ^ y == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x.Length != y.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(x[i]) && string.IsNullOrEmpty(y[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_valueComparer.Equals(x[i], y[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(string[] obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var hash = new HashCodeCombiner();
|
||||
for (var i = 0; i < obj.Length; i++)
|
||||
{
|
||||
var o = obj[i];
|
||||
|
||||
// Route values define null and "" to be equivalent.
|
||||
if (string.IsNullOrEmpty(o))
|
||||
{
|
||||
o = null;
|
||||
}
|
||||
hash.Add(o, _valueComparer);
|
||||
}
|
||||
|
||||
return hash.CombinedHash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
// 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.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal class StringArrayComparer : IEqualityComparer<string[]>
|
||||
{
|
||||
public static readonly StringArrayComparer Ordinal = new StringArrayComparer(StringComparer.Ordinal);
|
||||
|
||||
public static readonly StringArrayComparer OrdinalIgnoreCase = new StringArrayComparer(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly StringComparer _valueComparer;
|
||||
|
||||
private StringArrayComparer(StringComparer valueComparer)
|
||||
{
|
||||
_valueComparer = valueComparer;
|
||||
}
|
||||
|
||||
public bool Equals(string[] x, string[] y)
|
||||
{
|
||||
if (object.ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x == null ^ y == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x.Length != y.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(x[i]) && string.IsNullOrEmpty(y[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_valueComparer.Equals(x[i], y[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(string[] obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var hash = new HashCodeCombiner();
|
||||
for (var i = 0; i < obj.Length; i++)
|
||||
{
|
||||
var o = obj[i];
|
||||
|
||||
// Route values define null and "" to be equivalent.
|
||||
if (string.IsNullOrEmpty(o))
|
||||
{
|
||||
o = null;
|
||||
}
|
||||
hash.Add(o, _valueComparer);
|
||||
}
|
||||
|
||||
return hash.CombinedHash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,10 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
|
|||
<Compile Include="$(SharedSourceRoot)SecurityHelper\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\..\Http\Routing\src\DataSourceDependentCache.cs" Link="Routing\DataSourceDependentCache.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
|
||||
<Reference Include="Microsoft.AspNetCore.Authentication.Core" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,606 @@
|
|||
// 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.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
// The ActionSelectionTable has different code paths for ActionDescriptor and
|
||||
// RouteEndpoint for creating a table. We're trying to test both code paths
|
||||
// for creation, but selection works the same for both cases.
|
||||
public class ActionSelectionTableTest
|
||||
{
|
||||
[Fact]
|
||||
public void Select_SingleMatch()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "About" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(matches, (a) => Assert.Same(actions[0], a));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture("de-CH", "de-CH")]
|
||||
public void Select_ActionDescriptor_SingleMatch_UsesInvariantCulture()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" },
|
||||
{ "date", "10/31/2018 07:37:38 -07:00" },
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "About" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
values.Add(
|
||||
"date",
|
||||
new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)));
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(matches, (a) => Assert.Same(actions[0], a));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture("de-CH", "de-CH")]
|
||||
public void Select_Endpoint_SingleMatch_UsesInvariantCulture()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" },
|
||||
{ "date", "10/31/2018 07:37:38 -07:00" },
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "About" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithEndpoints(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
values.Add(
|
||||
"date",
|
||||
new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)));
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(matches, (e) => Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_ActionDescriptor_MultipleMatches()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(actions.ToArray(), matches.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Endpoint_MultipleMatches()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithEndpoints(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(actions.ToArray(), matches.Select(e => e.Metadata.GetMetadata<ActionDescriptor>()).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_NoMatch()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "About" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Foo", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_ActionDescriptors_NoMatch_ExcludesAttributeRoutedActions()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
AttributeRouteInfo = new AttributeRouteInfo()
|
||||
{
|
||||
Template = "/Home",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Endpoint_Match_IncludesAttributeRoutedActions()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
AttributeRouteInfo = new AttributeRouteInfo()
|
||||
{
|
||||
Template = "/Home",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithEndpoints(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Single(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Endpoint_NoMatch_ExcludesMatchingSuppressedAction()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
EndpointMetadata = new List<object>()
|
||||
{
|
||||
new SuppressMatchingMetadata(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var table = CreateTableWithEndpoints(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
// In this context `CaseSensitiveMatch` means that the input route values exactly match one of the action
|
||||
// descriptor's route values in terms of casing. This is important because we optimize for this case
|
||||
// in the implementation.
|
||||
[Fact]
|
||||
public void Select_Match_CaseSensitiveMatch_IncludesAllCaseInsensitiveMatches()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor() // This won't match the request
|
||||
{
|
||||
DisplayName = "A3",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "About" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var expected = actions.Take(2).ToArray();
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, matches);
|
||||
}
|
||||
|
||||
// In this context `CaseInsensitiveMatch` means that the input route values do not match any action
|
||||
// descriptor's route values in terms of casing. This is important because we optimize for the case
|
||||
// where the casing matches - the non-matching-casing path is handled a bit differently.
|
||||
[Fact]
|
||||
public void Select_Match_CaseInsensitiveMatch_IncludesAllCaseInsensitiveMatches()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A2",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
},
|
||||
new ActionDescriptor() // This won't match the request
|
||||
{
|
||||
DisplayName = "A3",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "About" }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var expected = actions.Take(2).ToArray();
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
var values = new RouteValueDictionary(new { controller = "HOME", action = "iNDex", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Match_CaseSensitiveMatch_MatchesOnEmptyString()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "area", null },
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
|
||||
// Example: In conventional route, one could set non-inline defaults
|
||||
// new { area = "", controller = "Home", action = "Index" }
|
||||
var values = new RouteValueDictionary(new { area = "", controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(matches);
|
||||
Assert.Same(actions[0], action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Match_CaseInsensitiveMatch_MatchesOnEmptyString()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "area", null },
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
|
||||
// Example: In conventional route, one could set non-inline defaults
|
||||
// new { area = "", controller = "Home", action = "Index" }
|
||||
var values = new RouteValueDictionary(new { area = "", controller = "HoMe", action = "InDeX", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(matches);
|
||||
Assert.Same(actions[0], action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Match_MatchesOnNull()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "area", null },
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
|
||||
// Example: In conventional route, one could set non-inline defaults
|
||||
// new { area = (string)null, controller = "Foo", action = "Index" }
|
||||
var values = new RouteValueDictionary(new { area = (string)null, controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(matches);
|
||||
Assert.Same(actions[0], action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Match_ActionDescriptorWithEmptyRouteValues_MatchesOnEmptyString()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "foo", "" },
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
|
||||
var values = new RouteValueDictionary(new { foo = "", controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(matches);
|
||||
Assert.Same(actions[0], action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_Match_ActionDescriptorWithEmptyRouteValues_MatchesOnNull()
|
||||
{
|
||||
var actions = new ActionDescriptor[]
|
||||
{
|
||||
new ActionDescriptor()
|
||||
{
|
||||
DisplayName = "A1",
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "foo", "" },
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var table = CreateTableWithActionDescriptors(actions);
|
||||
|
||||
var values = new RouteValueDictionary(new { foo = (string)null, controller = "Home", action = "Index", });
|
||||
|
||||
// Act
|
||||
var matches = table.Select(values);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(matches);
|
||||
Assert.Same(actions[0], action);
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<ActionDescriptor> CreateTableWithActionDescriptors(IReadOnlyList<ActionDescriptor> actions)
|
||||
{
|
||||
return ActionSelectionTable<ActionDescriptor>.Create(new ActionDescriptorCollection(actions, 0));
|
||||
}
|
||||
|
||||
private static ActionSelectionTable<RouteEndpoint> CreateTableWithEndpoints(IReadOnlyList<ActionDescriptor> actions)
|
||||
{
|
||||
|
||||
|
||||
var endpoints = actions.Select(a =>
|
||||
{
|
||||
var metadata = new List<object>(a.EndpointMetadata ?? Array.Empty<object>());
|
||||
metadata.Add(a);
|
||||
return new RouteEndpoint(
|
||||
requestDelegate: context => Task.CompletedTask,
|
||||
routePattern: RoutePatternFactory.Parse("/", defaults: a.RouteValues, parameterPolicies: null, requiredValues: a.RouteValues),
|
||||
order: 0,
|
||||
metadata: new EndpointMetadataCollection(metadata),
|
||||
a.DisplayName);
|
||||
});
|
||||
|
||||
return ActionSelectionTable<ActionDescriptor>.Create(endpoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue