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
|
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
|
internal sealed class DataSourceDependentCache<T> : IDisposable where T : class
|
||||||
{
|
{
|
||||||
private readonly EndpointDataSource _dataSource;
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.Internal;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
|
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
|
||||||
|
|
||||||
|
|
@ -22,13 +18,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ActionSelector : IActionSelector
|
internal class ActionSelector : IActionSelector
|
||||||
{
|
{
|
||||||
private static readonly IReadOnlyList<ActionDescriptor> EmptyActions = Array.Empty<ActionDescriptor>();
|
|
||||||
|
|
||||||
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
||||||
private readonly ActionConstraintCache _actionConstraintCache;
|
private readonly ActionConstraintCache _actionConstraintCache;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private Cache _cache;
|
private ActionSelectionTable<ActionDescriptor> _cache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="ActionSelector"/>.
|
/// Creates a new <see cref="ActionSelector"/>.
|
||||||
|
|
@ -49,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
_actionConstraintCache = actionConstraintCache;
|
_actionConstraintCache = actionConstraintCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cache Current
|
private ActionSelectionTable<ActionDescriptor> Current
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|
@ -61,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
cache = new Cache(actions);
|
cache = ActionSelectionTable<ActionDescriptor>.Create(actions);
|
||||||
Volatile.Write(ref _cache, cache);
|
Volatile.Write(ref _cache, cache);
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
@ -76,28 +70,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
||||||
var cache = Current;
|
var cache = Current;
|
||||||
|
|
||||||
// The Cache works based on a string[] of the route values in a pre-calculated order. This code extracts
|
var matches = cache.Select(context.RouteData.Values);
|
||||||
// those values in the correct order.
|
if (matches.Count > 0)
|
||||||
var keys = cache.RouteKeys;
|
|
||||||
var values = new string[keys.Length];
|
|
||||||
for (var i = 0; i < keys.Length; i++)
|
|
||||||
{
|
{
|
||||||
context.RouteData.Values.TryGetValue(keys[i], out var value);
|
return matches;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.NoActionsMatched(context.RouteData.Values);
|
_logger.NoActionsMatched(context.RouteData.Values);
|
||||||
return EmptyActions;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
|
public ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
|
||||||
|
|
@ -286,175 +266,5 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
return EvaluateActionConstraintsCore(context, actionsWithoutConstraint, order);
|
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" />
|
<Compile Include="$(SharedSourceRoot)SecurityHelper\**\*.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\..\Http\Routing\src\DataSourceDependentCache.cs" Link="Routing\DataSourceDependentCache.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
|
<Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Authentication.Core" />
|
<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