[Design] Split up MvcRouteHandler
This change splits up the conventional routing path from the attribute routing path *inside* routing, instead of inside `MvcRouteHandler`. Each attribute route group now gets its own instance of `MvcAttributeRouteHandler` which just knows about the actions it can reach. This removes the concept of a route-group-token and removes the lookup table entirely for attribute routing. This also means that the `DefaultHandler` on `IRouteBuilder` will not be used for attribute routes, which we are OK with for 1.0.0. The action selector's functionality is now split into two methods. We think this is OK for 1.0.0 because any customization of `IActionSelector` up to now had to implement virtually the same policy as ours in order to work with attribute routing. It should now be possible to customize the selector in a meaningful way without interfering with attribute routing.
This commit is contained in:
parent
62748b9471
commit
2e2784aa3d
|
|
@ -16,7 +16,6 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
|
||||||
Id = Guid.NewGuid().ToString();
|
Id = Guid.NewGuid().ToString();
|
||||||
Properties = new Dictionary<object, object>();
|
Properties = new Dictionary<object, object>();
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
RouteValueDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -32,8 +31,6 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
|
||||||
|
|
||||||
public AttributeRouteInfo AttributeRouteInfo { get; set; }
|
public AttributeRouteInfo AttributeRouteInfo { get; set; }
|
||||||
|
|
||||||
public IDictionary<string, object> RouteValueDefaults { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The set of constraints for this action. Must all be satisfied for the action to be selected.
|
/// The set of constraints for this action. Must all be satisfied for the action to be selected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.ActionConstraints
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A candidate action for action selection.
|
/// A candidate action for action selection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActionSelectorCandidate
|
public struct ActionSelectorCandidate
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="ActionSelectorCandidate"/>.
|
/// Creates a new <see cref="ActionSelectorCandidate"/>.
|
||||||
|
|
@ -33,11 +33,11 @@ namespace Microsoft.AspNetCore.Mvc.ActionConstraints
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="ActionDescriptor"/> representing a candiate for selection.
|
/// The <see cref="ActionDescriptor"/> representing a candiate for selection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ActionDescriptor Action { get; private set; }
|
public ActionDescriptor Action { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of <see cref="IActionConstraint"/> instances associated with <see name="Action"/>.
|
/// The list of <see cref="IActionConstraint"/> instances associated with <see name="Action"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<IActionConstraint> Constraints { get; private set; }
|
public IReadOnlyList<IActionConstraint> Constraints { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
return result.Substring(startIndex, subStringLength);
|
return result.Substring(startIndex, subStringLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ReplaceTokens(string template, IDictionary<string, object> values)
|
public static string ReplaceTokens(string template, IDictionary<string, string> values)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
var state = TemplateParserState.Plaintext;
|
var state = TemplateParserState.Plaintext;
|
||||||
|
|
@ -340,7 +340,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
.Replace("[[", "[")
|
.Replace("[[", "[")
|
||||||
.Replace("]]", "]");
|
.Replace("]]", "]");
|
||||||
|
|
||||||
object value;
|
string value;
|
||||||
if (!values.TryGetValue(token, out value))
|
if (!values.TryGetValue(token, out value))
|
||||||
{
|
{
|
||||||
// Value not found
|
// Value not found
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -94,11 +92,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
|
|
||||||
configureRoutes(routes);
|
configureRoutes(routes);
|
||||||
|
|
||||||
// Adding the attribute route comes after running the user-code because
|
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));
|
||||||
// we want to respect any changes to the DefaultHandler.
|
|
||||||
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
|
|
||||||
routes.DefaultHandler,
|
|
||||||
app.ApplicationServices));
|
|
||||||
|
|
||||||
return app.UseRouter(routes.Build());
|
return app.UseRouter(routes.Build());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstCandidate = context.Candidates[0];
|
var firstCandidate = context.Candidates[0];
|
||||||
if (firstCandidate != context.CurrentCandidate)
|
if (firstCandidate.Action != context.CurrentCandidate.Action)
|
||||||
{
|
{
|
||||||
// If the current candidate is not same as the first candidate,
|
// If the current candidate is not same as the first candidate,
|
||||||
// we need not probe other candidates to see if they apply.
|
// we need not probe other candidates to see if they apply.
|
||||||
|
|
@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
// 3). If we have no matches, then we choose the first constraint to return true.It will later return a 415
|
// 3). If we have no matches, then we choose the first constraint to return true.It will later return a 415
|
||||||
foreach (var candidate in context.Candidates)
|
foreach (var candidate in context.Candidates)
|
||||||
{
|
{
|
||||||
if (candidate == firstCandidate)
|
if (candidate.Action == firstCandidate.Action)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -217,9 +217,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<RedirectToRouteResultExecutor>();
|
services.TryAddSingleton<RedirectToRouteResultExecutor>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Setup default handler
|
// Route Handlers
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<MvcRouteHandler>();
|
services.TryAddSingleton<MvcRouteHandler>(); // Only one per app
|
||||||
|
services.TryAddTransient<MvcAttributeRouteHandler>(); // Many per app
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureDefaultServices(IServiceCollection services)
|
private static void ConfigureDefaultServices(IServiceCollection services)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
|
@ -12,13 +13,44 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
public interface IActionSelector
|
public interface IActionSelector
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selects an <see cref="ActionDescriptor"/> for the request associated with <paramref name="context"/>.
|
/// Selects a set of <see cref="ActionDescriptor"/> candidates for the current request associated with
|
||||||
|
/// <paramref name="context"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The <see cref="RouteContext"/> for the current request.</param>
|
/// <param name="context">The <see cref="RouteContext"/> associated with the current request.</param>
|
||||||
/// <returns>An <see cref="ActionDescriptor"/> or <c>null</c> if no action can be selected.</returns>
|
/// <returns>A set of <see cref="ActionDescriptor"/> candidates or <c>null</c>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Used by conventional routing to select the set of actions that match the route values for the
|
||||||
|
/// current request. Action constraints associated with the candidates are not invoked by this method
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Attribute routing does not call this method.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects the best <see cref="ActionDescriptor"/> candidate from <paramref name="candidates"/> for the
|
||||||
|
/// current request associated with <paramref name="context"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="RouteContext"/> associated with the current request.</param>
|
||||||
|
/// <param name="candidates">The set of <see cref="ActionDescriptor"/> candidates.</param>
|
||||||
|
/// <returns>The best <see cref="ActionDescriptor"/> candidate for the current request or <c>null</c>.</returns>
|
||||||
/// <exception cref="Internal.AmbiguousActionException">
|
/// <exception cref="Internal.AmbiguousActionException">
|
||||||
/// Thrown when action selection results in an ambiguity.
|
/// Thrown when action selection results in an ambiguity.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
ActionDescriptor Select(RouteContext context);
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Invokes action constraints associated with the candidates.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Used by conventional routing after calling <see cref="SelectCandidates"/> to apply action constraints and
|
||||||
|
/// disambiguate between multiple candidates.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Used by attribute routing to apply action constraints and disambiguate between multiple candidates.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ using System.Collections.Generic;
|
||||||
#if NET451
|
#if NET451
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
#endif
|
#endif
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.DecisionTree;
|
using Microsoft.AspNetCore.Routing.DecisionTree;
|
||||||
|
|
||||||
|
|
@ -27,8 +27,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
Version = actions.Version;
|
Version = actions.Version;
|
||||||
|
|
||||||
|
var conventionalRoutedActions = actions.Items.Where(a => a.AttributeRouteInfo?.Template == null).ToArray();
|
||||||
_root = DecisionTreeBuilder<ActionDescriptor>.GenerateTree(
|
_root = DecisionTreeBuilder<ActionDescriptor>.GenerateTree(
|
||||||
actions.Items,
|
conventionalRoutedActions,
|
||||||
new ActionDescriptorClassifier());
|
new ActionDescriptorClassifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
_actionConstraintCache = actionConstraintCache;
|
_actionConstraintCache = actionConstraintCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context)
|
||||||
public ActionDescriptor Select(RouteContext context)
|
|
||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
|
|
@ -48,35 +47,24 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
var tree = _decisionTreeProvider.DecisionTree;
|
var tree = _decisionTreeProvider.DecisionTree;
|
||||||
var matchingRouteValues = tree.Select(context.RouteData.Values);
|
return tree.Select(context.RouteData.Values);
|
||||||
|
}
|
||||||
|
|
||||||
var candidates = new List<ActionSelectorCandidate>();
|
public ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
|
||||||
|
{
|
||||||
// Perf: Avoid allocations
|
if (context == null)
|
||||||
for (var i = 0; i < matchingRouteValues.Count; i++)
|
|
||||||
{
|
{
|
||||||
var action = matchingRouteValues[i];
|
throw new ArgumentNullException(nameof(context));
|
||||||
var constraints = _actionConstraintCache.GetActionConstraints(context.HttpContext, action);
|
|
||||||
candidates.Add(new ActionSelectorCandidate(action, constraints));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchingActionConstraints =
|
if (candidates == null)
|
||||||
EvaluateActionConstraints(context, candidates, startingOrder: null);
|
|
||||||
|
|
||||||
List<ActionDescriptor> matchingActions = null;
|
|
||||||
if (matchingActionConstraints != null)
|
|
||||||
{
|
{
|
||||||
matchingActions = new List<ActionDescriptor>(matchingActionConstraints.Count);
|
throw new ArgumentNullException(nameof(candidates));
|
||||||
// Perf: Avoid allocations
|
|
||||||
for (var i = 0; i < matchingActionConstraints.Count; i++)
|
|
||||||
{
|
|
||||||
var candidate = matchingActionConstraints[i];
|
|
||||||
matchingActions.Add(candidate.Action);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalMatches = SelectBestActions(matchingActions);
|
var matches = EvaluateActionConstraints(context, candidates);
|
||||||
|
|
||||||
|
var finalMatches = SelectBestActions(matches);
|
||||||
if (finalMatches == null || finalMatches.Count == 0)
|
if (finalMatches == null || finalMatches.Count == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -113,7 +101,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<ActionSelectorCandidate> EvaluateActionConstraints(
|
private IReadOnlyList<ActionDescriptor> EvaluateActionConstraints(
|
||||||
|
RouteContext context,
|
||||||
|
IReadOnlyList<ActionDescriptor> actions)
|
||||||
|
{
|
||||||
|
var candidates = new List<ActionSelectorCandidate>();
|
||||||
|
|
||||||
|
// Perf: Avoid allocations
|
||||||
|
for (var i = 0; i < actions.Count; i++)
|
||||||
|
{
|
||||||
|
var action = actions[i];
|
||||||
|
var constraints = _actionConstraintCache.GetActionConstraints(context.HttpContext, action);
|
||||||
|
candidates.Add(new ActionSelectorCandidate(action, constraints));
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches = EvaluateActionConstraintsCore(context, candidates, startingOrder: null);
|
||||||
|
|
||||||
|
List<ActionDescriptor> results = null;
|
||||||
|
if (matches != null)
|
||||||
|
{
|
||||||
|
results = new List<ActionDescriptor>(matches.Count);
|
||||||
|
// Perf: Avoid allocations
|
||||||
|
for (var i = 0; i < matches.Count; i++)
|
||||||
|
{
|
||||||
|
var candidate = matches[i];
|
||||||
|
results.Add(candidate.Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<ActionSelectorCandidate> EvaluateActionConstraintsCore(
|
||||||
RouteContext context,
|
RouteContext context,
|
||||||
IReadOnlyList<ActionSelectorCandidate> candidates,
|
IReadOnlyList<ActionSelectorCandidate> candidates,
|
||||||
int? startingOrder)
|
int? startingOrder)
|
||||||
|
|
@ -198,7 +217,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// If we have matches with constraints, those are 'better' so try to keep processing those
|
// If we have matches with constraints, those are 'better' so try to keep processing those
|
||||||
if (actionsWithConstraint.Count > 0)
|
if (actionsWithConstraint.Count > 0)
|
||||||
{
|
{
|
||||||
var matches = EvaluateActionConstraints(context, actionsWithConstraint, order);
|
var matches = EvaluateActionConstraintsCore(context, actionsWithConstraint, order);
|
||||||
if (matches?.Count > 0)
|
if (matches?.Count > 0)
|
||||||
{
|
{
|
||||||
return matches;
|
return matches;
|
||||||
|
|
@ -212,7 +231,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return EvaluateActionConstraints(context, actionsWithoutConstraint, order);
|
return EvaluateActionConstraintsCore(context, actionsWithoutConstraint, order);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,32 +8,27 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Template;
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
using Microsoft.AspNetCore.Routing.Tree;
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
public class AttributeRoute : IRouter
|
public class AttributeRoute : IRouter
|
||||||
{
|
{
|
||||||
private readonly IRouter _handler;
|
|
||||||
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
||||||
private readonly IServiceProvider _services;
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly Func<ActionDescriptor[], IRouter> _handlerFactory;
|
||||||
|
|
||||||
private TreeRouter _router;
|
private TreeRouter _router;
|
||||||
|
|
||||||
public AttributeRoute(
|
public AttributeRoute(
|
||||||
IRouter handler,
|
|
||||||
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
|
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
|
||||||
IServiceProvider services)
|
IServiceProvider services,
|
||||||
|
Func<ActionDescriptor[], IRouter> handlerFactory)
|
||||||
{
|
{
|
||||||
if (handler == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(handler));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actionDescriptorCollectionProvider == null)
|
if (actionDescriptorCollectionProvider == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider));
|
throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider));
|
||||||
|
|
@ -44,9 +39,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(services));
|
throw new ArgumentNullException(nameof(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
_handler = handler;
|
if (handlerFactory == null)
|
||||||
|
{
|
||||||
|
_handlerFactory = handlerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
|
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
|
||||||
_services = services;
|
_services = services;
|
||||||
|
_handlerFactory = handlerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -88,10 +88,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// action by expected route values, and then use the TemplateBinder to generate the link.
|
// action by expected route values, and then use the TemplateBinder to generate the link.
|
||||||
foreach (var routeInfo in routeInfos)
|
foreach (var routeInfo in routeInfos)
|
||||||
{
|
{
|
||||||
|
var defaults = new RouteValueDictionary();
|
||||||
|
foreach (var kvp in routeInfo.ActionDescriptor.RouteValues)
|
||||||
|
{
|
||||||
|
defaults.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use the `NullRouter` as the route handler because we don't need to do anything for link
|
||||||
|
// generations. The TreeRouter does it all for us.
|
||||||
builder.MapOutbound(
|
builder.MapOutbound(
|
||||||
_handler,
|
NullRouter.Instance,
|
||||||
routeInfo.RouteTemplate,
|
routeInfo.RouteTemplate,
|
||||||
new RouteValueDictionary(routeInfo.ActionDescriptor.RouteValueDefaults),
|
defaults,
|
||||||
routeInfo.RouteName,
|
routeInfo.RouteName,
|
||||||
routeInfo.Order);
|
routeInfo.Order);
|
||||||
}
|
}
|
||||||
|
|
@ -99,37 +107,27 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// We're creating one AttributeRouteMatchingEntry per group, so we need to identify the distinct set of
|
// We're creating one AttributeRouteMatchingEntry per group, so we need to identify the distinct set of
|
||||||
// groups. It's guaranteed that all members of the group have the same template and precedence,
|
// groups. It's guaranteed that all members of the group have the same template and precedence,
|
||||||
// so we only need to hang on to a single instance of the RouteInfo for each group.
|
// so we only need to hang on to a single instance of the RouteInfo for each group.
|
||||||
var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos);
|
var groups = GroupRouteInfos(routeInfos);
|
||||||
foreach (var routeInfo in distinctRouteInfosByGroup)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
|
var handler = _handlerFactory(group.ToArray());
|
||||||
|
|
||||||
// Note that because we only support 'inline' defaults, each routeInfo group also has the same
|
// Note that because we only support 'inline' defaults, each routeInfo group also has the same
|
||||||
// set of defaults.
|
// set of defaults.
|
||||||
//
|
//
|
||||||
// We then inject the route group as a default for the matcher so it gets passed back to MVC
|
// We then inject the route group as a default for the matcher so it gets passed back to MVC
|
||||||
// for use in action selection.
|
// for use in action selection.
|
||||||
var entry = builder.MapInbound(
|
builder.MapInbound(
|
||||||
_handler,
|
handler,
|
||||||
routeInfo.RouteTemplate,
|
group.Key.RouteTemplate,
|
||||||
routeInfo.RouteName,
|
group.Key.RouteName,
|
||||||
routeInfo.Order);
|
group.Key.Order);
|
||||||
|
|
||||||
entry.Defaults[TreeRouter.RouteGroupKey] = routeInfo.RouteGroup;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos)
|
private static IEnumerable<IGrouping<RouteInfo, ActionDescriptor>> GroupRouteInfos(List<RouteInfo> routeInfos)
|
||||||
{
|
{
|
||||||
var routeInfosByGroupId = new Dictionary<string, RouteInfo>(StringComparer.OrdinalIgnoreCase);
|
return routeInfos.GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance);
|
||||||
|
|
||||||
foreach (var routeInfo in routeInfos)
|
|
||||||
{
|
|
||||||
if (!routeInfosByGroupId.ContainsKey(routeInfo.RouteGroup))
|
|
||||||
{
|
|
||||||
routeInfosByGroupId.Add(routeInfo.RouteGroup, routeInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return routeInfosByGroupId.Values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<RouteInfo> GetRouteInfos(IReadOnlyList<ActionDescriptor> actions)
|
private static List<RouteInfo> GetRouteInfos(IReadOnlyList<ActionDescriptor> actions)
|
||||||
|
|
@ -179,23 +177,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Dictionary<string, RouteTemplate> templateCache,
|
Dictionary<string, RouteTemplate> templateCache,
|
||||||
ActionDescriptor action)
|
ActionDescriptor action)
|
||||||
{
|
{
|
||||||
string value;
|
|
||||||
action.RouteValues.TryGetValue(TreeRouter.RouteGroupKey, out value);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
// This can happen if an ActionDescriptor has a route template, but doesn't have one of our
|
|
||||||
// special route group constraints. This is a good indication that the user is using a 3rd party
|
|
||||||
// routing system, or has customized their ADs in a way that we can no longer understand them.
|
|
||||||
//
|
|
||||||
// We just treat this case as an 'opt-out' of our attribute routing system.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var routeInfo = new RouteInfo()
|
var routeInfo = new RouteInfo()
|
||||||
{
|
{
|
||||||
ActionDescriptor = action,
|
ActionDescriptor = action,
|
||||||
RouteGroup = value,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -216,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return routeInfo;
|
return routeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var kvp in action.RouteValueDefaults)
|
foreach (var kvp in action.RouteValues)
|
||||||
{
|
{
|
||||||
foreach (var parameter in routeInfo.RouteTemplate.Parameters)
|
foreach (var parameter in routeInfo.RouteTemplate.Parameters)
|
||||||
{
|
{
|
||||||
|
|
@ -246,11 +230,66 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
public int Order { get; set; }
|
public int Order { get; set; }
|
||||||
|
|
||||||
public string RouteGroup { get; set; }
|
|
||||||
|
|
||||||
public string RouteName { get; set; }
|
public string RouteName { get; set; }
|
||||||
|
|
||||||
public RouteTemplate RouteTemplate { get; set; }
|
public RouteTemplate RouteTemplate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class RouteInfoEqualityComparer : IEqualityComparer<RouteInfo>
|
||||||
|
{
|
||||||
|
public static readonly RouteInfoEqualityComparer Instance = new RouteInfoEqualityComparer();
|
||||||
|
|
||||||
|
public bool Equals(RouteInfo x, RouteInfo y)
|
||||||
|
{
|
||||||
|
if (x == null && y == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (x == null ^ y == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (x.Order != y.Order)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return string.Equals(
|
||||||
|
x.RouteTemplate.TemplateText,
|
||||||
|
y.RouteTemplate.TemplateText,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(RouteInfo obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = new HashCodeCombiner();
|
||||||
|
hash.Add(obj.Order);
|
||||||
|
hash.Add(obj.RouteTemplate.TemplateText, StringComparer.OrdinalIgnoreCase);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used only to hook up link generation, and it doesn't need to do anything.
|
||||||
|
private class NullRouter : IRouter
|
||||||
|
{
|
||||||
|
public static readonly NullRouter Instance = new NullRouter();
|
||||||
|
|
||||||
|
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RouteAsync(RouteContext context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,25 +13,24 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an attribute route using the provided services and provided target router.
|
/// Creates an attribute route using the provided services and provided target router.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="target">The router to invoke when a route entry matches.</param>
|
|
||||||
/// <param name="services">The application services.</param>
|
/// <param name="services">The application services.</param>
|
||||||
/// <returns>An attribute route.</returns>
|
/// <returns>An attribute route.</returns>
|
||||||
public static IRouter CreateAttributeMegaRoute(IRouter target, IServiceProvider services)
|
public static IRouter CreateAttributeMegaRoute(IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (target == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (services == null)
|
if (services == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(services));
|
throw new ArgumentNullException(nameof(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AttributeRoute(
|
return new AttributeRoute(
|
||||||
target,
|
|
||||||
services.GetRequiredService<IActionDescriptorCollectionProvider>(),
|
services.GetRequiredService<IActionDescriptorCollectionProvider>(),
|
||||||
services);
|
services,
|
||||||
|
actions =>
|
||||||
|
{
|
||||||
|
var handler = services.GetRequiredService<MvcAttributeRouteHandler>();
|
||||||
|
handler.Actions = actions;
|
||||||
|
return handler;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
var actions = new List<ControllerActionDescriptor>();
|
var actions = new List<ControllerActionDescriptor>();
|
||||||
|
|
||||||
var hasAttributeRoutes = false;
|
|
||||||
var routeValueKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var routeValueKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var methodInfoMap = new MethodToActionMap();
|
var methodInfoMap = new MethodToActionMap();
|
||||||
|
|
@ -71,21 +70,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
AddProperties(actionDescriptor, action, controller, application);
|
AddProperties(actionDescriptor, action, controller, application);
|
||||||
|
|
||||||
actionDescriptor.BoundProperties = controllerPropertyDescriptors;
|
actionDescriptor.BoundProperties = controllerPropertyDescriptors;
|
||||||
|
|
||||||
if (IsAttributeRoutedAction(actionDescriptor))
|
if (IsAttributeRoutedAction(actionDescriptor))
|
||||||
{
|
{
|
||||||
hasAttributeRoutes = true;
|
|
||||||
|
|
||||||
// An attribute routed action will ignore conventional routed constraints. We still
|
|
||||||
// want to provide these values as ambient values for link generation.
|
|
||||||
AddRouteValuesAsDefaultRouteValues(actionDescriptor);
|
|
||||||
|
|
||||||
// Replaces tokens like [controller]/[action] in the route template with the actual values
|
// Replaces tokens like [controller]/[action] in the route template with the actual values
|
||||||
// for this action.
|
// for this action.
|
||||||
ReplaceAttributeRouteTokens(actionDescriptor, routeTemplateErrors);
|
ReplaceAttributeRouteTokens(actionDescriptor, routeTemplateErrors);
|
||||||
|
|
||||||
// Attribute routed actions will ignore conventional routed values. Instead they have
|
|
||||||
// a single route value "RouteGroup" associated with it.
|
|
||||||
ReplaceRouteValues(actionDescriptor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,44 +103,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
validatedMethods.Add(actionDescriptor.MethodInfo);
|
validatedMethods.Add(actionDescriptor.MethodInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsAttributeRoutedAction(actionDescriptor))
|
var attributeRouteInfo = actionDescriptor.AttributeRouteInfo;
|
||||||
|
if (attributeRouteInfo?.Name != null)
|
||||||
{
|
{
|
||||||
// Any attribute routes are in use, then non-attribute-routed action descriptors can't be
|
// Build a map of attribute route name to action descriptors to ensure that all
|
||||||
// selected when a route group returned by the route.
|
// attribute routes with a given name have the same template.
|
||||||
if (hasAttributeRoutes)
|
AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
|
||||||
{
|
|
||||||
actionDescriptor.RouteValues.Add(TreeRouter.RouteGroupKey, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a route value with 'null' for each user-defined route value in the set to all the
|
|
||||||
// actions that don't have that value. For example, if a controller defines
|
|
||||||
// an area, all actions that don't belong to an area must have a route
|
|
||||||
// value that prevents them from matching an incoming request when area is specified.
|
|
||||||
AddGlobalRouteValues(actionDescriptor, routeValueKeys);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var attributeRouteInfo = actionDescriptor.AttributeRouteInfo;
|
|
||||||
if (attributeRouteInfo.Name != null)
|
|
||||||
{
|
|
||||||
// Build a map of attribute route name to action descriptors to ensure that all
|
|
||||||
// attribute routes with a given name have the same template.
|
|
||||||
AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We still want to add a 'null' for any constraint with DenyKey so that link generation
|
// Add a route value with 'null' for each user-defined route value in the set to all the
|
||||||
// works properly.
|
// actions that don't have that value. For example, if a controller defines
|
||||||
//
|
// an area, all actions that don't belong to an area must have a route
|
||||||
// Consider an action like { area = "", controller = "Home", action = "Index" }. Even if
|
// value that prevents them from matching an incoming request when area is specified.
|
||||||
// it's attribute routed, it needs to know that area must be null to generate a link.
|
AddGlobalRouteValues(actionDescriptor, routeValueKeys);
|
||||||
foreach (var key in routeValueKeys)
|
|
||||||
{
|
|
||||||
if (!actionDescriptor.RouteValueDefaults.ContainsKey(key))
|
|
||||||
{
|
|
||||||
actionDescriptor.RouteValueDefaults.Add(key, value: null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attributeRoutingConfigurationErrors.Any())
|
if (attributeRoutingConfigurationErrors.Any())
|
||||||
|
|
@ -490,16 +455,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReplaceRouteValues(ControllerActionDescriptor actionDescriptor)
|
|
||||||
{
|
|
||||||
var routeGroupValue = GetRouteGroupValue(
|
|
||||||
actionDescriptor.AttributeRouteInfo.Order,
|
|
||||||
actionDescriptor.AttributeRouteInfo.Template);
|
|
||||||
|
|
||||||
actionDescriptor.RouteValues.Clear();
|
|
||||||
actionDescriptor.RouteValues.Add(TreeRouter.RouteGroupKey, routeGroupValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReplaceAttributeRouteTokens(
|
private static void ReplaceAttributeRouteTokens(
|
||||||
ControllerActionDescriptor actionDescriptor,
|
ControllerActionDescriptor actionDescriptor,
|
||||||
IList<string> routeTemplateErrors)
|
IList<string> routeTemplateErrors)
|
||||||
|
|
@ -508,13 +463,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens(
|
actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens(
|
||||||
actionDescriptor.AttributeRouteInfo.Template,
|
actionDescriptor.AttributeRouteInfo.Template,
|
||||||
actionDescriptor.RouteValueDefaults);
|
actionDescriptor.RouteValues);
|
||||||
|
|
||||||
if (actionDescriptor.AttributeRouteInfo.Name != null)
|
if (actionDescriptor.AttributeRouteInfo.Name != null)
|
||||||
{
|
{
|
||||||
actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens(
|
actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens(
|
||||||
actionDescriptor.AttributeRouteInfo.Name,
|
actionDescriptor.AttributeRouteInfo.Name,
|
||||||
actionDescriptor.RouteValueDefaults);
|
actionDescriptor.RouteValues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
|
|
@ -530,14 +485,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddRouteValuesAsDefaultRouteValues(ControllerActionDescriptor actionDescriptor)
|
|
||||||
{
|
|
||||||
foreach (var kvp in actionDescriptor.RouteValues)
|
|
||||||
{
|
|
||||||
actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddGlobalRouteValues(
|
private static void AddGlobalRouteValues(
|
||||||
ControllerActionDescriptor actionDescriptor,
|
ControllerActionDescriptor actionDescriptor,
|
||||||
ISet<string> removalConstraints)
|
ISet<string> removalConstraints)
|
||||||
|
|
@ -731,12 +678,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetRouteGroupValue(int order, string template)
|
|
||||||
{
|
|
||||||
var group = string.Format(CultureInfo.InvariantCulture, "{0}-{1}", order, template);
|
|
||||||
return ("__route__" + group).ToUpperInvariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to build a map of methods to reflected actions and reflected actions to
|
// We need to build a map of methods to reflected actions and reflected actions to
|
||||||
// action descriptors so that we can validate later that no method produced attribute
|
// action descriptors so that we can validate later that no method produced attribute
|
||||||
// and non attributed actions at the same time, and that no method that produced attribute
|
// and non attributed actions at the same time, and that no method that produced attribute
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
public class MvcAttributeRouteHandler : IRouter
|
||||||
|
{
|
||||||
|
private IActionContextAccessor _actionContextAccessor;
|
||||||
|
private IActionInvokerFactory _actionInvokerFactory;
|
||||||
|
private IActionSelector _actionSelector;
|
||||||
|
private ILogger _logger;
|
||||||
|
private DiagnosticSource _diagnosticSource;
|
||||||
|
|
||||||
|
public MvcAttributeRouteHandler(
|
||||||
|
IActionInvokerFactory actionInvokerFactory,
|
||||||
|
IActionSelector actionSelector,
|
||||||
|
DiagnosticSource diagnosticSource,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
|
: this(actionInvokerFactory, actionSelector, diagnosticSource, loggerFactory, actionContextAccessor: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MvcAttributeRouteHandler(
|
||||||
|
IActionInvokerFactory actionInvokerFactory,
|
||||||
|
IActionSelector actionSelector,
|
||||||
|
DiagnosticSource diagnosticSource,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IActionContextAccessor actionContextAccessor)
|
||||||
|
{
|
||||||
|
// The IActionContextAccessor is optional. We want to avoid the overhead of using CallContext
|
||||||
|
// if possible.
|
||||||
|
_actionContextAccessor = actionContextAccessor;
|
||||||
|
|
||||||
|
_actionInvokerFactory = actionInvokerFactory;
|
||||||
|
_actionSelector = actionSelector;
|
||||||
|
_diagnosticSource = diagnosticSource;
|
||||||
|
_logger = loggerFactory.CreateLogger<MvcAttributeRouteHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionDescriptor[] Actions { get; set; }
|
||||||
|
|
||||||
|
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We return null here because we're not responsible for generating the url, the route is.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RouteAsync(RouteContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Actions == null)
|
||||||
|
{
|
||||||
|
var message = Resources.FormatPropertyOfTypeCannotBeNull(
|
||||||
|
nameof(Actions),
|
||||||
|
nameof(MvcAttributeRouteHandler));
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionDescriptor = _actionSelector.SelectBestCandidate(context, Actions);
|
||||||
|
if (actionDescriptor == null)
|
||||||
|
{
|
||||||
|
_logger.NoActionsMatched();
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in actionDescriptor.RouteValues)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(kvp.Value))
|
||||||
|
{
|
||||||
|
context.RouteData.Values[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Handler = (c) =>
|
||||||
|
{
|
||||||
|
var routeData = c.GetRouteData();
|
||||||
|
|
||||||
|
var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
|
||||||
|
if (_actionContextAccessor != null)
|
||||||
|
{
|
||||||
|
_actionContextAccessor.ActionContext = actionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
|
||||||
|
if (invoker == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatActionInvokerFactory_CouldNotCreateInvoker(
|
||||||
|
actionDescriptor.DisplayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoker.InvokeAsync();
|
||||||
|
};
|
||||||
|
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Tree;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
@ -64,28 +63,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionDescriptor = _actionSelector.Select(context);
|
var candidates = _actionSelector.SelectCandidates(context);
|
||||||
|
if (candidates == null || candidates.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.NoActionsMatched();
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionDescriptor = _actionSelector.SelectBestCandidate(context, candidates);
|
||||||
if (actionDescriptor == null)
|
if (actionDescriptor == null)
|
||||||
{
|
{
|
||||||
_logger.NoActionsMatched();
|
_logger.NoActionsMatched();
|
||||||
return TaskCache.CompletedTask;
|
return TaskCache.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionDescriptor.RouteValueDefaults != null)
|
context.Handler = (c) =>
|
||||||
{
|
|
||||||
foreach (var kvp in actionDescriptor.RouteValueDefaults)
|
|
||||||
{
|
|
||||||
if (!context.RouteData.Values.ContainsKey(kvp.Key))
|
|
||||||
{
|
|
||||||
context.RouteData.Values.Add(kvp.Key, kvp.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removing RouteGroup from RouteValues to simulate the result of conventional routing
|
|
||||||
context.RouteData.Values.Remove(TreeRouter.RouteGroupKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Handler = async (c) =>
|
|
||||||
{
|
{
|
||||||
var routeData = c.GetRouteData();
|
var routeData = c.GetRouteData();
|
||||||
|
|
||||||
|
|
@ -103,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
actionDescriptor.DisplayName));
|
actionDescriptor.DisplayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
await invoker.InvokeAsync();
|
return invoker.InvokeAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
return TaskCache.CompletedTask;
|
return TaskCache.CompletedTask;
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The casing of a route value in <see cref="ActionContext.RouteData"/> is determined by the client.
|
/// The casing of a route value in <see cref="ActionContext.RouteData"/> is determined by the client.
|
||||||
/// This making constructing paths for view locations in a case sensitive file system unreliable. Using the
|
/// This making constructing paths for view locations in a case sensitive file system unreliable. Using the
|
||||||
/// <see cref="Abstractions.ActionDescriptor.RouteValueDefaults"/> for attribute routes and
|
/// <see cref="Abstractions.ActionDescriptor.RouteValues"/> to get route values
|
||||||
/// <see cref="Abstractions.ActionDescriptor.RouteValues"/> for traditional routes to get route values
|
|
||||||
/// produces consistently cased results.
|
/// produces consistently cased results.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static string GetNormalizedRouteValue(ActionContext context, string key)
|
public static string GetNormalizedRouteValue(ActionContext context, string key)
|
||||||
|
|
@ -115,22 +114,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
var actionDescriptor = context.ActionDescriptor;
|
var actionDescriptor = context.ActionDescriptor;
|
||||||
string normalizedValue = null;
|
string normalizedValue = null;
|
||||||
if (actionDescriptor.AttributeRouteInfo != null)
|
|
||||||
|
string value;
|
||||||
|
if (actionDescriptor.RouteValues.TryGetValue(key, out value) &&
|
||||||
|
!string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
object match;
|
normalizedValue = value;
|
||||||
if (actionDescriptor.RouteValueDefaults.TryGetValue(key, out match))
|
|
||||||
{
|
|
||||||
normalizedValue = match?.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string value;
|
|
||||||
if (actionDescriptor.RouteValues.TryGetValue(key, out value) &&
|
|
||||||
!string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
normalizedValue = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringRouteValue = routeValue?.ToString();
|
var stringRouteValue = routeValue?.ToString();
|
||||||
|
|
|
||||||
|
|
@ -179,22 +179,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
|
|
||||||
var actionDescriptor = context.ActionDescriptor;
|
var actionDescriptor = context.ActionDescriptor;
|
||||||
string normalizedValue = null;
|
string normalizedValue = null;
|
||||||
if (actionDescriptor.AttributeRouteInfo != null)
|
string value;
|
||||||
|
if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out value) &&
|
||||||
|
!string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
object match;
|
normalizedValue = value;
|
||||||
if (actionDescriptor.RouteValueDefaults.TryGetValue(ActionNameKey, out match))
|
|
||||||
{
|
|
||||||
normalizedValue = match?.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string value;
|
|
||||||
if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out value) &&
|
|
||||||
!string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
normalizedValue = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringRouteValue = routeValue?.ToString();
|
var stringRouteValue = routeValue?.ToString();
|
||||||
|
|
|
||||||
|
|
@ -193,22 +193,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
|
|
||||||
var actionDescriptor = context.ActionDescriptor;
|
var actionDescriptor = context.ActionDescriptor;
|
||||||
string normalizedValue = null;
|
string normalizedValue = null;
|
||||||
if (actionDescriptor.AttributeRouteInfo != null)
|
string value;
|
||||||
|
if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out value) &&
|
||||||
|
!string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
object match;
|
normalizedValue = value;
|
||||||
if (actionDescriptor.RouteValueDefaults.TryGetValue(ActionNameKey, out match))
|
|
||||||
{
|
|
||||||
normalizedValue = match?.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string value;
|
|
||||||
if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out value) &&
|
|
||||||
!string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
normalizedValue = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringRouteValue = routeValue?.ToString();
|
var stringRouteValue = routeValue?.ToString();
|
||||||
|
|
|
||||||
|
|
@ -146,17 +146,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(ReplaceTokens_ValueValuesData))]
|
[MemberData(nameof(ReplaceTokens_ValueValuesData))]
|
||||||
public void ReplaceTokens_ValidValues(string template, object values, string expected)
|
public void ReplaceTokens_ValidValues(string template, Dictionary<string, string> values, string expected)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var valuesDictionary = values as IDictionary<string, object>;
|
|
||||||
if (valuesDictionary == null)
|
|
||||||
{
|
|
||||||
valuesDictionary = new RouteValueDictionary(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = AttributeRouteModel.ReplaceTokens(template, valuesDictionary);
|
var result = AttributeRouteModel.ReplaceTokens(template, values);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expected, result);
|
Assert.Equal(expected, result);
|
||||||
|
|
@ -164,15 +158,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(ReplaceTokens_InvalidFormatValuesData))]
|
[MemberData(nameof(ReplaceTokens_InvalidFormatValuesData))]
|
||||||
public void ReplaceTokens_InvalidFormat(string template, object values, string reason)
|
public void ReplaceTokens_InvalidFormat(string template, Dictionary<string, string> values, string reason)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var valuesDictionary = values as IDictionary<string, object>;
|
|
||||||
if (valuesDictionary == null)
|
|
||||||
{
|
|
||||||
valuesDictionary = new RouteValueDictionary(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
var expected = string.Format(
|
var expected = string.Format(
|
||||||
"The route template '{0}' has invalid syntax. {1}",
|
"The route template '{0}' has invalid syntax. {1}",
|
||||||
template,
|
template,
|
||||||
|
|
@ -180,7 +168,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var ex = Assert.Throws<InvalidOperationException>(
|
var ex = Assert.Throws<InvalidOperationException>(
|
||||||
() => { AttributeRouteModel.ReplaceTokens(template, valuesDictionary); });
|
() => { AttributeRouteModel.ReplaceTokens(template, values); });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expected, ex.Message);
|
Assert.Equal(expected, ex.Message);
|
||||||
|
|
@ -191,7 +179,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var template = "[area]/[controller]/[action2]";
|
var template = "[area]/[controller]/[action2]";
|
||||||
var values = new RouteValueDictionary()
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "area", "Help" },
|
{ "area", "Help" },
|
||||||
{ "controller", "Admin" },
|
{ "controller", "Admin" },
|
||||||
|
|
@ -428,49 +416,73 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[controller]/[action]",
|
"[controller]/[action]",
|
||||||
new { controller = "Home", action = "Index" },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" }
|
||||||
|
},
|
||||||
"Home/Index"
|
"Home/Index"
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[controller]",
|
"[controller]",
|
||||||
new { controller = "Home", action = "Index" },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" }
|
||||||
|
},
|
||||||
"Home"
|
"Home"
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[controller][[",
|
"[controller][[",
|
||||||
new { controller = "Home", action = "Index" },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" }
|
||||||
|
},
|
||||||
"Home["
|
"Home["
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[coNTroller]",
|
"[coNTroller]",
|
||||||
new { contrOLler = "Home", action = "Index" },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" }
|
||||||
|
},
|
||||||
"Home"
|
"Home"
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"thisisSomeText[action]",
|
"thisisSomeText[action]",
|
||||||
new { controller = "Home", action = "Index" },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" }
|
||||||
|
},
|
||||||
"thisisSomeTextIndex"
|
"thisisSomeTextIndex"
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[[-]][[/[[controller]]",
|
"[[-]][[/[[controller]]",
|
||||||
new { controller = "Home", action = "Index" },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" }
|
||||||
|
},
|
||||||
"[-][/[controller]"
|
"[-][/[controller]"
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[contr[[oller]/[act]]ion]",
|
"[contr[[oller]/[act]]ion]",
|
||||||
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "contr[oller", "Home" },
|
{ "contr[oller", "Home" },
|
||||||
{ "act]ion", "Index" }
|
{ "act]ion", "Index" }
|
||||||
|
|
@ -481,7 +493,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[controller][action]",
|
"[controller][action]",
|
||||||
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "controller", "Home" },
|
{ "controller", "Home" },
|
||||||
{ "action", "Index" }
|
{ "action", "Index" }
|
||||||
|
|
@ -492,7 +504,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[contr}oller]/[act{ion]/{id}",
|
"[contr}oller]/[act{ion]/{id}",
|
||||||
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "contr}oller", "Home" },
|
{ "contr}oller", "Home" },
|
||||||
{ "act{ion", "Index" }
|
{ "act{ion", "Index" }
|
||||||
|
|
@ -509,35 +521,35 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[",
|
"[",
|
||||||
new { },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
"A replacement token is not closed."
|
"A replacement token is not closed."
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"text]",
|
"text]",
|
||||||
new { },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
"Token delimiters ('[', ']') are imbalanced.",
|
"Token delimiters ('[', ']') are imbalanced.",
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"text]morecooltext",
|
"text]morecooltext",
|
||||||
new { },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
"Token delimiters ('[', ']') are imbalanced.",
|
"Token delimiters ('[', ']') are imbalanced.",
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[action",
|
"[action",
|
||||||
new { },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
"A replacement token is not closed.",
|
"A replacement token is not closed.",
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[action]]][",
|
"[action]]][",
|
||||||
new RouteValueDictionary()
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "action]", "Index" }
|
{ "action]", "Index" }
|
||||||
},
|
},
|
||||||
|
|
@ -547,21 +559,21 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[action]]",
|
"[action]]",
|
||||||
new { },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
"A replacement token is not closed."
|
"A replacement token is not closed."
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[ac[tion]",
|
"[ac[tion]",
|
||||||
new { },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
"An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape."
|
"An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape."
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new object[]
|
yield return new object[]
|
||||||
{
|
{
|
||||||
"[]",
|
"[]",
|
||||||
new { },
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
"An empty replacement token ('[]') is not allowed.",
|
"An empty replacement token ('[]') is not allowed.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
|
@ -22,10 +21,159 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
|
// Most of the in-depth testing for SelectCandidates is part of the descision tree tests.
|
||||||
|
// This is just basic coverage of the API in common scenarios.
|
||||||
public class DefaultActionSelectorTests
|
public class DefaultActionSelectorTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_AmbiguousActions_LogIsCorrect()
|
public void SelectCandidates_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 selector = CreateSelector(actions);
|
||||||
|
|
||||||
|
var routeContext = CreateRouteContext("GET");
|
||||||
|
routeContext.RouteData.Values.Add("controller", "Home");
|
||||||
|
routeContext.RouteData.Values.Add("action", "Index");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.SelectCandidates(routeContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(candidates, (a) => Assert.Same(actions[0], a));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectCandidates_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 selector = CreateSelector(actions);
|
||||||
|
|
||||||
|
var routeContext = CreateRouteContext("GET");
|
||||||
|
routeContext.RouteData.Values.Add("controller", "Home");
|
||||||
|
routeContext.RouteData.Values.Add("action", "Index");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.SelectCandidates(routeContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(actions.ToArray(), candidates.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectCandidates_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 selector = CreateSelector(actions);
|
||||||
|
|
||||||
|
var routeContext = CreateRouteContext("GET");
|
||||||
|
routeContext.RouteData.Values.Add("controller", "Foo");
|
||||||
|
routeContext.RouteData.Values.Add("action", "Index");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.SelectCandidates(routeContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectCandidates_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 selector = CreateSelector(actions);
|
||||||
|
|
||||||
|
var routeContext = CreateRouteContext("GET");
|
||||||
|
routeContext.RouteData.Values.Add("controller", "Home");
|
||||||
|
routeContext.RouteData.Values.Add("action", "Index");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.SelectCandidates(routeContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectBestCandidate_AmbiguousActions_LogIsCorrect()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var sink = new TestSink();
|
var sink = new TestSink();
|
||||||
|
|
@ -44,7 +192,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
$"ambiguity. Matching actions: {actionNames}";
|
$"ambiguity. Matching actions: {actionNames}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
Assert.Throws<AmbiguousActionException>(() => { selector.Select(routeContext); });
|
Assert.Throws<AmbiguousActionException>(() => { selector.SelectBestCandidate(routeContext, actions); });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(sink.Scopes);
|
Assert.Empty(sink.Scopes);
|
||||||
|
|
@ -53,7 +201,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_PrefersActionWithConstraints()
|
public void SelectBestCandidate_PrefersActionWithConstraints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionWithConstraints = new ActionDescriptor()
|
var actionWithConstraints = new ActionDescriptor()
|
||||||
|
|
@ -76,14 +224,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(action, actionWithConstraints);
|
Assert.Same(action, actionWithConstraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_ConstraintsRejectAll()
|
public void SelectBestCandidate_ConstraintsRejectAll()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action1 = new ActionDescriptor()
|
var action1 = new ActionDescriptor()
|
||||||
|
|
@ -108,14 +256,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Null(action);
|
Assert.Null(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_ConstraintsRejectAll_DifferentStages()
|
public void SelectBestCandidate_ConstraintsRejectAll_DifferentStages()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action1 = new ActionDescriptor()
|
var action1 = new ActionDescriptor()
|
||||||
|
|
@ -142,14 +290,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Null(action);
|
Assert.Null(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_ActionConstraintFactory()
|
public void SelectBestCandidate_ActionConstraintFactory()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionWithConstraints = new ActionDescriptor()
|
var actionWithConstraints = new ActionDescriptor()
|
||||||
|
|
@ -174,14 +322,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(action, actionWithConstraints);
|
Assert.Same(action, actionWithConstraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_ActionConstraintFactory_ReturnsNull()
|
public void SelectBestCandidate_ActionConstraintFactory_ReturnsNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var nullConstraint = new ActionDescriptor()
|
var nullConstraint = new ActionDescriptor()
|
||||||
|
|
@ -200,7 +348,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(action, nullConstraint);
|
Assert.Same(action, nullConstraint);
|
||||||
|
|
@ -208,7 +356,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
||||||
// There's a custom constraint provider registered that only understands BooleanConstraintMarker
|
// There's a custom constraint provider registered that only understands BooleanConstraintMarker
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_CustomProvider()
|
public void SelectBestCandidate_CustomProvider()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionWithConstraints = new ActionDescriptor()
|
var actionWithConstraints = new ActionDescriptor()
|
||||||
|
|
@ -230,7 +378,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(action, actionWithConstraints);
|
Assert.Same(action, actionWithConstraints);
|
||||||
|
|
@ -238,7 +386,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
||||||
// Due to ordering of stages, the first action will be better.
|
// Due to ordering of stages, the first action will be better.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_ConstraintsInOrder()
|
public void SelectBestCandidate_ConstraintsInOrder()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var best = new ActionDescriptor()
|
var best = new ActionDescriptor()
|
||||||
|
|
@ -263,7 +411,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(action, best);
|
Assert.Same(action, best);
|
||||||
|
|
@ -271,7 +419,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
||||||
// Due to ordering of stages, the first action will be better.
|
// Due to ordering of stages, the first action will be better.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_ConstraintsInOrder_MultipleStages()
|
public void SelectBestCandidate_ConstraintsInOrder_MultipleStages()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var best = new ActionDescriptor()
|
var best = new ActionDescriptor()
|
||||||
|
|
@ -300,14 +448,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(action, best);
|
Assert.Same(action, best);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_Fallback_ToActionWithoutConstraints()
|
public void SelectBestCandidate_Fallback_ToActionWithoutConstraints()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var nomatch1 = new ActionDescriptor()
|
var nomatch1 = new ActionDescriptor()
|
||||||
|
|
@ -338,14 +486,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
var context = CreateRouteContext("POST");
|
var context = CreateRouteContext("POST");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var action = selector.Select(context);
|
var action = selector.SelectBestCandidate(context, actions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(action, best);
|
Assert.Same(action, best);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Select_Ambiguous()
|
public void SelectBestCandidate_Ambiguous()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expectedMessage =
|
var expectedMessage =
|
||||||
|
|
@ -359,7 +507,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
CreateAction(area: null, controller: "Store", action: "Buy"),
|
CreateAction(area: null, controller: "Store", action: "Buy"),
|
||||||
CreateAction(area: null, controller: "Store", action: "Buy"),
|
CreateAction(area: null, controller: "Store", action: "Buy"),
|
||||||
CreateAction(area: null, controller: "Store", action: "Cart"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
actions[0].DisplayName = "Ambiguous1";
|
actions[0].DisplayName = "Ambiguous1";
|
||||||
|
|
@ -374,7 +521,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
// Act
|
// Act
|
||||||
var ex = Assert.Throws<AmbiguousActionException>(() =>
|
var ex = Assert.Throws<AmbiguousActionException>(() =>
|
||||||
{
|
{
|
||||||
selector.Select(context);
|
selector.SelectBestCandidate(context, actions);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -528,12 +675,13 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
new DefaultActionConstraintProvider(),
|
new DefaultActionConstraintProvider(),
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaultActionSelector = new ActionSelector(
|
var actionSelector = new ActionSelector(
|
||||||
decisionTreeProvider,
|
decisionTreeProvider,
|
||||||
GetActionConstraintCache(actionConstraintProviders),
|
GetActionConstraintCache(actionConstraintProviders),
|
||||||
NullLoggerFactory.Instance);
|
NullLoggerFactory.Instance);
|
||||||
|
|
||||||
return (ControllerActionDescriptor)defaultActionSelector.Select(context);
|
var candidates = actionSelector.SelectCandidates(context);
|
||||||
|
return (ControllerActionDescriptor)actionSelector.SelectBestCandidate(context, candidates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControllerActionDescriptorProvider GetActionDescriptorProvider()
|
private ControllerActionDescriptorProvider GetActionDescriptorProvider()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -28,8 +29,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
||||||
var mockActionSelector = new Mock<IActionSelector>();
|
var mockActionSelector = new Mock<IActionSelector>();
|
||||||
mockActionSelector
|
mockActionSelector
|
||||||
.Setup(a => a.Select(It.IsAny<RouteContext>()))
|
.Setup(a => a.SelectCandidates(It.IsAny<RouteContext>()))
|
||||||
.Returns<ActionDescriptor>(null);
|
.Returns(new ActionDescriptor[0]);
|
||||||
|
|
||||||
var context = CreateRouteContext();
|
var context = CreateRouteContext();
|
||||||
|
|
||||||
|
|
@ -48,39 +49,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
Assert.Equal(expectedMessage, sink.Writes[0].State?.ToString());
|
Assert.Equal(expectedMessage, sink.Writes[0].State?.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RouteHandler_RemovesRouteGroupFromRouteValues()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var invoker = new Mock<IActionInvoker>();
|
|
||||||
invoker
|
|
||||||
.Setup(i => i.InvokeAsync())
|
|
||||||
.Returns(Task.FromResult(true));
|
|
||||||
|
|
||||||
var invokerFactory = new Mock<IActionInvokerFactory>();
|
|
||||||
invokerFactory
|
|
||||||
.Setup(f => f.CreateInvoker(It.IsAny<ActionContext>()))
|
|
||||||
.Returns<ActionContext>((c) =>
|
|
||||||
{
|
|
||||||
return invoker.Object;
|
|
||||||
});
|
|
||||||
|
|
||||||
var context = CreateRouteContext();
|
|
||||||
var handler = CreateMvcRouteHandler(invokerFactory: invokerFactory.Object);
|
|
||||||
|
|
||||||
var originalRouteData = context.RouteData;
|
|
||||||
originalRouteData.Values.Add(TreeRouter.RouteGroupKey, "/Home/Test");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await handler.RouteAsync(context);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Same(originalRouteData, context.RouteData);
|
|
||||||
|
|
||||||
|
|
||||||
Assert.False(context.RouteData.Values.ContainsKey(TreeRouter.RouteGroupKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
private MvcRouteHandler CreateMvcRouteHandler(
|
private MvcRouteHandler CreateMvcRouteHandler(
|
||||||
ActionDescriptor actionDescriptor = null,
|
ActionDescriptor actionDescriptor = null,
|
||||||
IActionSelector actionSelector = null,
|
IActionSelector actionSelector = null,
|
||||||
|
|
@ -99,7 +67,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
if (actionSelector == null)
|
if (actionSelector == null)
|
||||||
{
|
{
|
||||||
var mockActionSelector = new Mock<IActionSelector>();
|
var mockActionSelector = new Mock<IActionSelector>();
|
||||||
mockActionSelector.Setup(a => a.Select(It.IsAny<RouteContext>()))
|
mockActionSelector
|
||||||
|
.Setup(a => a.SelectCandidates(It.IsAny<RouteContext>()))
|
||||||
|
.Returns(new ActionDescriptor[] { actionDescriptor });
|
||||||
|
|
||||||
|
mockActionSelector
|
||||||
|
.Setup(a => a.SelectBestCandidate(It.IsAny<RouteContext>(), It.IsAny<IReadOnlyList<ActionDescriptor>>()))
|
||||||
.Returns(actionDescriptor);
|
.Returns(actionDescriptor);
|
||||||
actionSelector = mockActionSelector.Object;
|
actionSelector = mockActionSelector.Object;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public async Task AttributeRoute_UsesUpdatedActionDescriptors()
|
public async Task AttributeRoute_UsesUpdatedActionDescriptors()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var handler = CreateHandler();
|
ActionDescriptor selected = null;
|
||||||
|
|
||||||
var actions = new List<ActionDescriptor>()
|
var actions = new List<ActionDescriptor>()
|
||||||
{
|
{
|
||||||
|
|
@ -40,28 +40,45 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
AttributeRouteInfo = new AttributeRouteInfo()
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
{
|
{
|
||||||
Template = "api/Blog/{id}"
|
Template = "api/Blog/{key1}"
|
||||||
},
|
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new ActionDescriptor()
|
new ActionDescriptor()
|
||||||
{
|
{
|
||||||
AttributeRouteInfo = new AttributeRouteInfo()
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
{
|
{
|
||||||
Template = "api/Store/Buy/{id}"
|
Template = "api/Store/Buy/{key2}"
|
||||||
},
|
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "2" }
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Func<ActionDescriptor[], IRouter> handlerFactory = (_) =>
|
||||||
|
{
|
||||||
|
var handler = new Mock<IRouter>();
|
||||||
|
handler
|
||||||
|
.Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
|
||||||
|
.Returns<RouteContext>(routeContext =>
|
||||||
|
{
|
||||||
|
if (routeContext.RouteData.Values.ContainsKey("key1"))
|
||||||
|
{
|
||||||
|
selected = actions[0];
|
||||||
|
}
|
||||||
|
else if (routeContext.RouteData.Values.ContainsKey("key2"))
|
||||||
|
{
|
||||||
|
selected = actions[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
routeContext.Handler = (c) => TaskCache.CompletedTask;
|
||||||
|
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
|
||||||
|
});
|
||||||
|
return handler.Object;
|
||||||
|
};
|
||||||
|
|
||||||
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
var route = CreateRoute(handler.Object, actionDescriptorProvider.Object);
|
var route = CreateRoute(handlerFactory, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
var requestServices = new Mock<IServiceProvider>(MockBehavior.Strict);
|
var requestServices = new Mock<IServiceProvider>(MockBehavior.Strict);
|
||||||
requestServices
|
requestServices
|
||||||
|
|
@ -79,12 +96,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
// Assert 1
|
// Assert 1
|
||||||
Assert.NotNull(context.Handler);
|
Assert.NotNull(context.Handler);
|
||||||
Assert.Equal("5", context.RouteData.Values["id"]);
|
Assert.Equal("5", context.RouteData.Values["key2"]);
|
||||||
Assert.Equal("2", context.RouteData.Values[TreeRouter.RouteGroupKey]);
|
Assert.Same(actions[1], selected);
|
||||||
|
|
||||||
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
|
||||||
|
|
||||||
// Arrange 2 - remove the action and update the collection
|
// Arrange 2 - remove the action and update the collection
|
||||||
|
selected = null;
|
||||||
actions.RemoveAt(1);
|
actions.RemoveAt(1);
|
||||||
actionDescriptorProvider
|
actionDescriptorProvider
|
||||||
.SetupGet(ad => ad.ActionDescriptors)
|
.SetupGet(ad => ad.ActionDescriptors)
|
||||||
|
|
@ -98,8 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// Assert 2
|
// Assert 2
|
||||||
Assert.Null(context.Handler);
|
Assert.Null(context.Handler);
|
||||||
Assert.Empty(context.RouteData.Values);
|
Assert.Empty(context.RouteData.Values);
|
||||||
|
Assert.Null(selected);
|
||||||
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -117,10 +132,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -145,7 +156,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal(17, e.Order);
|
Assert.Equal(17, e.Order);
|
||||||
Assert.Equal(actions[0].RouteValueDefaults.ToArray(), e.RequiredLinkValues.ToArray());
|
Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
|
||||||
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -165,10 +176,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -193,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal(17, e.Order);
|
Assert.Equal(17, e.Order);
|
||||||
Assert.Equal(actions[0].RouteValueDefaults.ToArray(), e.RequiredLinkValues.ToArray());
|
Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
|
||||||
Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -213,10 +220,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -241,7 +244,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal(17, e.Order);
|
Assert.Equal(17, e.Order);
|
||||||
Assert.Equal(actions[0].RouteValueDefaults.ToArray(), e.RequiredLinkValues.ToArray());
|
Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
|
||||||
Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -264,10 +267,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -282,10 +281,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index2" },
|
{ "action", "Index2" },
|
||||||
|
|
@ -310,7 +305,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal(17, e.Order);
|
Assert.Equal(17, e.Order);
|
||||||
Assert.Equal(actions[0].RouteValueDefaults.ToArray(), e.RequiredLinkValues.ToArray());
|
Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
|
||||||
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
||||||
},
|
},
|
||||||
e =>
|
e =>
|
||||||
|
|
@ -320,7 +315,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal(17, e.Order);
|
Assert.Equal(17, e.Order);
|
||||||
Assert.Equal(actions[1].RouteValueDefaults.ToArray(), e.RequiredLinkValues.ToArray());
|
Assert.Equal(ToRouteValueDictionary(actions[1].RouteValues), e.RequiredLinkValues);
|
||||||
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -340,10 +335,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -368,9 +359,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
||||||
Assert.Collection(
|
Assert.Empty(e.Defaults);
|
||||||
e.Defaults.OrderBy(kvp => kvp.Key),
|
|
||||||
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,10 +378,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -417,9 +402,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
|
||||||
Assert.Collection(
|
Assert.Empty(e.Defaults);
|
||||||
e.Defaults.OrderBy(kvp => kvp.Key),
|
|
||||||
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -438,10 +421,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -468,7 +447,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
e.Defaults.OrderBy(kvp => kvp.Key),
|
e.Defaults.OrderBy(kvp => kvp.Key),
|
||||||
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp),
|
|
||||||
kvp => Assert.Equal(new KeyValuePair<string, object>("slug", "hello"), kvp));
|
kvp => Assert.Equal(new KeyValuePair<string, object>("slug", "hello"), kvp));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -491,10 +469,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index" },
|
{ "action", "Index" },
|
||||||
|
|
@ -509,10 +483,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Order = 17,
|
Order = 17,
|
||||||
},
|
},
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "1" }
|
|
||||||
},
|
|
||||||
RouteValueDefaults = new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
{ "controller", "Blog" },
|
{ "controller", "Blog" },
|
||||||
{ "action", "Index2" },
|
{ "action", "Index2" },
|
||||||
|
|
@ -537,9 +507,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
|
Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
|
||||||
Assert.Equal("BLOG_INDEX", e.RouteName);
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
||||||
Assert.Collection(
|
Assert.Empty(e.Defaults);
|
||||||
e.Defaults.OrderBy(kvp => kvp.Key),
|
|
||||||
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -580,6 +548,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
private static AttributeRoute CreateRoute(
|
private static AttributeRoute CreateRoute(
|
||||||
IRouter handler,
|
IRouter handler,
|
||||||
IActionDescriptorCollectionProvider actionDescriptorProvider)
|
IActionDescriptorCollectionProvider actionDescriptorProvider)
|
||||||
|
{
|
||||||
|
return CreateRoute((_) => handler, actionDescriptorProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttributeRoute CreateRoute(
|
||||||
|
Func<ActionDescriptor[], IRouter> handlerFactory,
|
||||||
|
IActionDescriptorCollectionProvider actionDescriptorProvider)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection()
|
var services = new ServiceCollection()
|
||||||
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
|
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
|
||||||
|
|
@ -588,7 +563,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
.AddRouting()
|
.AddRouting()
|
||||||
.AddOptions()
|
.AddOptions()
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
return new AttributeRoute(handler, actionDescriptorProvider, services);
|
return new AttributeRoute(actionDescriptorProvider, services, handlerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needed because new RouteValueDictionary(values) would give us all the properties of
|
||||||
|
// the Dictionary class.
|
||||||
|
private static RouteValueDictionary ToRouteValueDictionary(IDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
var result = new RouteValueDictionary();
|
||||||
|
foreach (var kvp in values)
|
||||||
|
{
|
||||||
|
result.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Tree;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
|
@ -44,10 +43,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
"and can occur only at the start of the parameter." + Environment.NewLine +
|
"and can occur only at the start of the parameter." + Environment.NewLine +
|
||||||
"Parameter name: routeTemplate";
|
"Parameter name: routeTemplate";
|
||||||
|
|
||||||
var handler = CreateRouter();
|
|
||||||
var services = CreateServices(action);
|
var services = CreateServices(action);
|
||||||
|
|
||||||
var route = AttributeRouting.CreateAttributeMegaRoute(handler, services);
|
var route = AttributeRouting.CreateAttributeMegaRoute(services);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
|
@ -63,7 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action = CreateAction("DisallowedParameter", "{foo}/{action}");
|
var action = CreateAction("DisallowedParameter", "{foo}/{action}");
|
||||||
action.RouteValueDefaults.Add("foo", "bleh");
|
action.RouteValues.Add("foo", "bleh");
|
||||||
|
|
||||||
var expectedMessage =
|
var expectedMessage =
|
||||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||||
|
|
@ -72,10 +70,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
"Error: The attribute route '{foo}/{action}' cannot contain a parameter named '{foo}'. " +
|
"Error: The attribute route '{foo}/{action}' cannot contain a parameter named '{foo}'. " +
|
||||||
"Use '[foo]' in the route template to insert the value 'bleh'.";
|
"Use '[foo]' in the route template to insert the value 'bleh'.";
|
||||||
|
|
||||||
var handler = CreateRouter();
|
|
||||||
var services = CreateServices(action);
|
var services = CreateServices(action);
|
||||||
|
|
||||||
var route = AttributeRouting.CreateAttributeMegaRoute(handler, services);
|
var route = AttributeRouting.CreateAttributeMegaRoute(services);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
|
@ -91,10 +88,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action1 = CreateAction("DisallowedParameter1", "{foo}/{action}");
|
var action1 = CreateAction("DisallowedParameter1", "{foo}/{action}");
|
||||||
action1.RouteValueDefaults.Add("foo", "bleh");
|
action1.RouteValues.Add("foo", "bleh");
|
||||||
|
|
||||||
var action2 = CreateAction("DisallowedParameter2", "cool/{action}");
|
var action2 = CreateAction("DisallowedParameter2", "cool/{action}");
|
||||||
action2.RouteValueDefaults.Add("action", "hey");
|
action2.RouteValues.Add("action", "hey");
|
||||||
|
|
||||||
var expectedMessage =
|
var expectedMessage =
|
||||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||||
|
|
@ -107,10 +104,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
"Error: The attribute route 'cool/{action}' cannot contain a parameter named '{action}'. " +
|
"Error: The attribute route 'cool/{action}' cannot contain a parameter named '{action}'. " +
|
||||||
"Use '[action]' in the route template to insert the value 'hey'.";
|
"Use '[action]' in the route template to insert the value 'hey'.";
|
||||||
|
|
||||||
var handler = CreateRouter();
|
|
||||||
var services = CreateServices(action1, action2);
|
var services = CreateServices(action1, action2);
|
||||||
|
|
||||||
var route = AttributeRouting.CreateAttributeMegaRoute(handler, services);
|
var route = AttributeRouting.CreateAttributeMegaRoute(services);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
|
@ -133,14 +129,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
action.MethodInfo = actionMethod;
|
action.MethodInfo = actionMethod;
|
||||||
action.RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
action.RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ TreeRouter.RouteGroupKey, "group" }
|
{ "controller", "Home" },
|
||||||
|
{ "action", "Index" },
|
||||||
};
|
};
|
||||||
action.AttributeRouteInfo = new AttributeRouteInfo();
|
action.AttributeRouteInfo = new AttributeRouteInfo();
|
||||||
action.AttributeRouteInfo.Template = "{controller}/{action}";
|
action.AttributeRouteInfo.Template = "{controller}/{action}";
|
||||||
|
|
||||||
action.RouteValueDefaults.Add("controller", "Home");
|
|
||||||
action.RouteValueDefaults.Add("action", "Index");
|
|
||||||
|
|
||||||
var expectedMessage =
|
var expectedMessage =
|
||||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||||
Environment.NewLine +
|
Environment.NewLine +
|
||||||
|
|
@ -148,10 +142,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
"Error: The attribute route '{controller}/{action}' cannot contain a parameter named '{controller}'. " +
|
"Error: The attribute route '{controller}/{action}' cannot contain a parameter named '{controller}'. " +
|
||||||
"Use '[controller]' in the route template to insert the value 'Home'.";
|
"Use '[controller]' in the route template to insert the value 'Home'.";
|
||||||
|
|
||||||
var handler = CreateRouter();
|
|
||||||
var services = CreateServices(action);
|
var services = CreateServices(action);
|
||||||
|
|
||||||
var route = AttributeRouting.CreateAttributeMegaRoute(handler, services);
|
var route = AttributeRouting.CreateAttributeMegaRoute(services);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
|
@ -167,19 +160,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return new DisplayNameActionDescriptor()
|
return new DisplayNameActionDescriptor()
|
||||||
{
|
{
|
||||||
DisplayName = displayName,
|
DisplayName = displayName,
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, "whatever" }
|
|
||||||
},
|
|
||||||
AttributeRouteInfo = new AttributeRouteInfo { Template = template },
|
AttributeRouteInfo = new AttributeRouteInfo { Template = template },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IRouter CreateRouter()
|
|
||||||
{
|
|
||||||
return Mock.Of<IRouter>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IServiceProvider CreateServices(params ActionDescriptor[] actions)
|
private static IServiceProvider CreateServices(params ActionDescriptor[] actions)
|
||||||
{
|
{
|
||||||
var collection = new ActionDescriptorCollection(actions, version: 0);
|
var collection = new ActionDescriptorCollection(actions, version: 0);
|
||||||
|
|
|
||||||
|
|
@ -261,12 +261,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// Assert
|
// Assert
|
||||||
var action = Assert.Single(descriptors);
|
var action = Assert.Single(descriptors);
|
||||||
|
|
||||||
Assert.Equal(TreeRouter.RouteGroupKey, Assert.Single(action.RouteValues).Key);
|
var controller = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("controller"));
|
||||||
|
|
||||||
var controller = Assert.Single(action.RouteValueDefaults, kvp => kvp.Key.Equals("controller"));
|
|
||||||
Assert.Equal("AttributeRouted", controller.Value);
|
Assert.Equal("AttributeRouted", controller.Value);
|
||||||
|
|
||||||
var actionConstraint = Assert.Single(action.RouteValueDefaults, kvp => kvp.Key.Equals("action"));
|
var actionConstraint = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("action"));
|
||||||
Assert.Equal(nameof(AttributeRoutedController.AttributeRoutedAction), actionConstraint.Value);
|
Assert.Equal(nameof(AttributeRoutedController.AttributeRoutedAction), actionConstraint.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -916,30 +914,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal(expectedMessage, ex.Message);
|
Assert.Equal(expectedMessage, ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AttributeRouting_RouteGroupConstraint_IsAddedOnceForNonAttributeRoutes()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var provider = GetProvider(
|
|
||||||
typeof(ConventionalAndAttributeRoutedActionsWithAreaController).GetTypeInfo(),
|
|
||||||
typeof(ConstrainedController).GetTypeInfo());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var actionDescriptors = provider.GetDescriptors();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(actionDescriptors);
|
|
||||||
Assert.Equal(4, actionDescriptors.Count());
|
|
||||||
|
|
||||||
foreach (var actionDescriptor in actionDescriptors.Where(ad => ad.AttributeRouteInfo == null))
|
|
||||||
{
|
|
||||||
Assert.Equal(6, actionDescriptor.RouteValues.Count);
|
|
||||||
Assert.Single(
|
|
||||||
actionDescriptor.RouteValues,
|
|
||||||
kvp => kvp.Key.Equals(TreeRouter.RouteGroupKey) && string.IsNullOrEmpty(kvp.Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AttributeRouting_AddsDefaultRouteValues_ForAttributeRoutedActions()
|
public void AttributeRouting_AddsDefaultRouteValues_ForAttributeRoutedActions()
|
||||||
{
|
{
|
||||||
|
|
@ -957,27 +931,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
var indexAction = Assert.Single(actionDescriptors, ad => ad.ActionName.Equals("Index"));
|
var indexAction = Assert.Single(actionDescriptors, ad => ad.ActionName.Equals("Index"));
|
||||||
|
|
||||||
Assert.Equal(1, indexAction.RouteValues.Count);
|
Assert.Equal(5, indexAction.RouteValues.Count);
|
||||||
|
|
||||||
var routeGroup = Assert.Single(indexAction.RouteValues, kvp => kvp.Key.Equals(TreeRouter.RouteGroupKey));
|
var controllerDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("controller", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.NotNull(routeGroup.Value);
|
|
||||||
|
|
||||||
Assert.Equal(5, indexAction.RouteValueDefaults.Count);
|
|
||||||
|
|
||||||
var controllerDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("controller", StringComparison.OrdinalIgnoreCase));
|
|
||||||
Assert.Equal("ConventionalAndAttributeRoutedActionsWithArea", controllerDefault.Value);
|
Assert.Equal("ConventionalAndAttributeRoutedActionsWithArea", controllerDefault.Value);
|
||||||
|
|
||||||
var actionDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("action", StringComparison.OrdinalIgnoreCase));
|
var actionDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("action", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.Equal("Index", actionDefault.Value);
|
Assert.Equal("Index", actionDefault.Value);
|
||||||
|
|
||||||
var areaDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("area", StringComparison.OrdinalIgnoreCase));
|
var areaDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("area", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.Equal("Home", areaDefault.Value);
|
Assert.Equal("Home", areaDefault.Value);
|
||||||
|
|
||||||
var mvRouteValueDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("key", StringComparison.OrdinalIgnoreCase));
|
var mvRouteValueDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("key", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.Null(mvRouteValueDefault.Value);
|
Assert.Equal(string.Empty, mvRouteValueDefault.Value);
|
||||||
|
|
||||||
var anotherRouteValue = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("second", StringComparison.OrdinalIgnoreCase));
|
var anotherRouteValue = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("second", StringComparison.OrdinalIgnoreCase));
|
||||||
Assert.Null(anotherRouteValue.Value);
|
Assert.Equal(string.Empty, anotherRouteValue.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -994,29 +963,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Equal("stub/ThisIsAnAction", action.AttributeRouteInfo.Template);
|
Assert.Equal("stub/ThisIsAnAction", action.AttributeRouteInfo.Template);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token replacement happens before we 'group' routes. So two route templates
|
|
||||||
// that are equivalent after token replacement go to the same 'group'.
|
|
||||||
[Fact]
|
|
||||||
public void AttributeRouting_TokenReplacement_BeforeGroupId()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var provider = GetProvider(typeof(SameGroupIdController).GetTypeInfo());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var actions = provider.GetDescriptors().ToArray();
|
|
||||||
|
|
||||||
var groupIds = actions.Select(
|
|
||||||
a => a.RouteValues
|
|
||||||
.Where(kvp => kvp.Key == TreeRouter.RouteGroupKey)
|
|
||||||
.Select(kvp => kvp.Value)
|
|
||||||
.Single())
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(2, groupIds.Length);
|
|
||||||
Assert.Equal(groupIds[0], groupIds[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameters are validated later. This action uses the forbidden {action} and {controller}
|
// Parameters are validated later. This action uses the forbidden {action} and {controller}
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AttributeRouting_DoesNotValidateParameters()
|
public void AttributeRouting_DoesNotValidateParameters()
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
typeof(RouteCollection).FullName,
|
typeof(RouteCollection).FullName,
|
||||||
typeof(AttributeRoute).FullName,
|
typeof(AttributeRoute).FullName,
|
||||||
typeof(MvcRouteHandler).FullName,
|
typeof(MvcAttributeRouteHandler).FullName,
|
||||||
},
|
},
|
||||||
result.Routers);
|
result.Routers);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1148,12 +1148,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
Assert.Equal(expected, result.SearchedLocations);
|
Assert.Equal(expected, result.SearchedLocations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
// Looks in RouteValueDefaults
|
public void FindPage_SelectsActionCaseInsensitively()
|
||||||
[InlineData(true)]
|
|
||||||
// Looks in RouteValues
|
|
||||||
[InlineData(false)]
|
|
||||||
public void FindPage_SelectsActionCaseInsensitively(bool isAttributeRouted)
|
|
||||||
{
|
{
|
||||||
// The ActionDescriptor contains "Foo" and the RouteData contains "foo"
|
// The ActionDescriptor contains "Foo" and the RouteData contains "foo"
|
||||||
// which matches the case of the constructor thus searching in the appropriate location.
|
// which matches the case of the constructor thus searching in the appropriate location.
|
||||||
|
|
@ -1177,8 +1173,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
|
|
||||||
var context = GetActionContextWithActionDescriptor(
|
var context = GetActionContextWithActionDescriptor(
|
||||||
routeValues,
|
routeValues,
|
||||||
routesInActionDescriptor,
|
routesInActionDescriptor);
|
||||||
isAttributeRouted);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = viewEngine.FindPage(context, "details");
|
var result = viewEngine.FindPage(context, "details");
|
||||||
|
|
@ -1190,12 +1185,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
pageFactory.Verify();
|
pageFactory.Verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
// Looks in RouteValueDefaults
|
public void FindPage_LooksForPages_UsingActionDescriptor_Controller()
|
||||||
[InlineData(true)]
|
|
||||||
// Looks in RouteValues
|
|
||||||
[InlineData(false)]
|
|
||||||
public void FindPage_LooksForPages_UsingActionDescriptor_Controller(bool isAttributeRouted)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new[]
|
var expected = new[]
|
||||||
|
|
@ -1216,8 +1207,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
var viewEngine = CreateViewEngine();
|
var viewEngine = CreateViewEngine();
|
||||||
var context = GetActionContextWithActionDescriptor(
|
var context = GetActionContextWithActionDescriptor(
|
||||||
routeValues,
|
routeValues,
|
||||||
routesInActionDescriptor,
|
routesInActionDescriptor);
|
||||||
isAttributeRouted);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = viewEngine.FindPage(context, "foo");
|
var result = viewEngine.FindPage(context, "foo");
|
||||||
|
|
@ -1228,12 +1218,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
Assert.Equal(expected, result.SearchedLocations);
|
Assert.Equal(expected, result.SearchedLocations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
// Looks in RouteValueDefaults
|
public void FindPage_LooksForPages_UsingActionDescriptor_Areas()
|
||||||
[InlineData(true)]
|
|
||||||
// Looks in RouteValues
|
|
||||||
[InlineData(false)]
|
|
||||||
public void FindPage_LooksForPages_UsingActionDescriptor_Areas(bool isAttributeRouted)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new[]
|
var expected = new[]
|
||||||
|
|
@ -1257,8 +1243,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
var viewEngine = CreateViewEngine();
|
var viewEngine = CreateViewEngine();
|
||||||
var context = GetActionContextWithActionDescriptor(
|
var context = GetActionContextWithActionDescriptor(
|
||||||
routeValues,
|
routeValues,
|
||||||
routesInActionDescriptor,
|
routesInActionDescriptor);
|
||||||
isAttributeRouted);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = viewEngine.FindPage(context, "foo");
|
var result = viewEngine.FindPage(context, "foo");
|
||||||
|
|
@ -1269,10 +1254,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
Assert.Equal(expected, result.SearchedLocations);
|
Assert.Equal(expected, result.SearchedLocations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[InlineData(true)]
|
public void FindPage_LooksForPages_UsesRouteValuesAsFallback()
|
||||||
[InlineData(false)]
|
|
||||||
public void FindPage_LooksForPages_UsesRouteValuesAsFallback(bool isAttributeRouted)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new[]
|
var expected = new[]
|
||||||
|
|
@ -1289,8 +1272,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
var viewEngine = CreateViewEngine();
|
var viewEngine = CreateViewEngine();
|
||||||
var context = GetActionContextWithActionDescriptor(
|
var context = GetActionContextWithActionDescriptor(
|
||||||
routeValues,
|
routeValues,
|
||||||
new Dictionary<string, string>(),
|
new Dictionary<string, string>());
|
||||||
isAttributeRouted);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = viewEngine.FindPage(context, "bar");
|
var result = viewEngine.FindPage(context, "bar");
|
||||||
|
|
@ -1462,7 +1444,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetNormalizedRouteValue_ReturnsValueFromRouteValues_IfKeyHandlingIsRequired()
|
public void GetNormalizedRouteValue_ReturnsValueFromRouteValues()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var key = "some-key";
|
var key = "some-key";
|
||||||
|
|
@ -1530,112 +1512,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
Assert.Equal("route-value", result);
|
Assert.Equal("route-value", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetNormalizedRouteValue_UsesRouteValueDefaults_IfAttributeRouted()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "some-key";
|
|
||||||
var actionDescriptor = new ActionDescriptor
|
|
||||||
{
|
|
||||||
AttributeRouteInfo = new AttributeRouteInfo(),
|
|
||||||
};
|
|
||||||
actionDescriptor.RouteValueDefaults[key] = "Route-Value";
|
|
||||||
|
|
||||||
var actionContext = new ActionContext
|
|
||||||
{
|
|
||||||
ActionDescriptor = actionDescriptor,
|
|
||||||
RouteData = new RouteData()
|
|
||||||
};
|
|
||||||
|
|
||||||
actionContext.RouteData.Values[key] = "route-value";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("Route-Value", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetNormalizedRouteValue_UsesRouteValue_IfRouteValueDefaultsDoesNotMatchRouteValue()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "some-key";
|
|
||||||
var actionDescriptor = new ActionDescriptor
|
|
||||||
{
|
|
||||||
AttributeRouteInfo = new AttributeRouteInfo(),
|
|
||||||
};
|
|
||||||
actionDescriptor.RouteValueDefaults[key] = "different-value";
|
|
||||||
|
|
||||||
var actionContext = new ActionContext
|
|
||||||
{
|
|
||||||
ActionDescriptor = actionDescriptor,
|
|
||||||
RouteData = new RouteData()
|
|
||||||
};
|
|
||||||
|
|
||||||
actionContext.RouteData.Values[key] = "route-value";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("route-value", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetNormalizedRouteValue_ConvertsRouteDefaultToStringValue_IfAttributeRouted()
|
|
||||||
{
|
|
||||||
using (new CultureReplacer())
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "some-key";
|
|
||||||
var actionDescriptor = new ActionDescriptor
|
|
||||||
{
|
|
||||||
AttributeRouteInfo = new AttributeRouteInfo(),
|
|
||||||
};
|
|
||||||
actionDescriptor.RouteValueDefaults[key] = 32;
|
|
||||||
|
|
||||||
var actionContext = new ActionContext
|
|
||||||
{
|
|
||||||
ActionDescriptor = actionDescriptor,
|
|
||||||
RouteData = new RouteData()
|
|
||||||
};
|
|
||||||
|
|
||||||
actionContext.RouteData.Values[key] = 32;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("32", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetNormalizedRouteValue_UsesRouteDataValue_IfKeyDoesNotExistInRouteDefaultValues()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "some-key";
|
|
||||||
var actionDescriptor = new ActionDescriptor
|
|
||||||
{
|
|
||||||
AttributeRouteInfo = new AttributeRouteInfo(),
|
|
||||||
};
|
|
||||||
|
|
||||||
var actionContext = new ActionContext
|
|
||||||
{
|
|
||||||
ActionDescriptor = actionDescriptor,
|
|
||||||
RouteData = new RouteData()
|
|
||||||
};
|
|
||||||
|
|
||||||
actionContext.RouteData.Values[key] = "route-value";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("route-value", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetNormalizedRouteValue_ConvertsRouteValueToString()
|
public void GetNormalizedRouteValue_ConvertsRouteValueToString()
|
||||||
{
|
{
|
||||||
|
|
@ -1743,8 +1619,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
|
|
||||||
private static ActionContext GetActionContextWithActionDescriptor(
|
private static ActionContext GetActionContextWithActionDescriptor(
|
||||||
IDictionary<string, object> routeValues,
|
IDictionary<string, object> routeValues,
|
||||||
IDictionary<string, string> actionRouteValues,
|
IDictionary<string, string> actionRouteValues)
|
||||||
bool isAttributeRouted)
|
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
var routeData = new RouteData();
|
var routeData = new RouteData();
|
||||||
|
|
@ -1754,20 +1629,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionDescriptor = new ActionDescriptor();
|
var actionDescriptor = new ActionDescriptor();
|
||||||
if (isAttributeRouted)
|
|
||||||
|
foreach (var kvp in actionRouteValues)
|
||||||
{
|
{
|
||||||
actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo();
|
actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
|
||||||
foreach (var kvp in actionRouteValues)
|
|
||||||
{
|
|
||||||
actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var kvp in actionRouteValues)
|
|
||||||
{
|
|
||||||
actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ActionContext(httpContext, routeData, actionDescriptor);
|
return new ActionContext(httpContext, routeData, actionDescriptor);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
@model BasicWebSite.Models.RemoteAttributeUser
|
@model BasicWebSite.Models.RemoteAttributeUser
|
||||||
@{
|
@{
|
||||||
Layout = "_Layout.cshtml";
|
Layout = "_Layout.cshtml";
|
||||||
object areaObject;
|
object areaName;
|
||||||
ViewContext.ActionDescriptor.RouteValueDefaults.TryGetValue("area", out areaObject);
|
ViewContext.RouteData.Values.TryGetValue("area", out areaName);
|
||||||
var areaName = (areaObject as string) ?? "root";
|
areaName = areaName ?? "root";
|
||||||
ViewBag.Title = "Create in " + areaName + " area.";
|
ViewBag.Title = "Create in " + areaName + " area.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
@model BasicWebSite.Models.RemoteAttributeUser
|
@model BasicWebSite.Models.RemoteAttributeUser
|
||||||
@{
|
@{
|
||||||
Layout = "_Layout.cshtml";
|
Layout = "_Layout.cshtml";
|
||||||
object areaObject;
|
object areaName;
|
||||||
ViewContext.ActionDescriptor.RouteValueDefaults.TryGetValue("area", out areaObject);
|
ViewContext.RouteData.Values.TryGetValue("area", out areaName);
|
||||||
var areaName = (areaObject as string) ?? "root";
|
areaName = areaName ?? "root";
|
||||||
ViewBag.Title = "Details in " + areaName + " area.";
|
ViewBag.Title = "Details in " + areaName + " area.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
@model BasicWebSite.Models.RemoteAttributeUser
|
@model BasicWebSite.Models.RemoteAttributeUser
|
||||||
@{
|
@{
|
||||||
Layout = "_Layout.cshtml";
|
Layout = "_Layout.cshtml";
|
||||||
object areaObject;
|
object areaName;
|
||||||
ViewContext.ActionDescriptor.RouteValueDefaults.TryGetValue("area", out areaObject);
|
ViewContext.RouteData.Values.TryGetValue("area", out areaName);
|
||||||
var areaName = (areaObject as string) ?? "root";
|
areaName = areaName ?? "root";
|
||||||
ViewBag.Title = "Create in " + areaName + " area.";
|
ViewBag.Title = "Create in " + areaName + " area.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
@model BasicWebSite.Models.RemoteAttributeUser
|
@model BasicWebSite.Models.RemoteAttributeUser
|
||||||
@{
|
@{
|
||||||
Layout = "_Layout.cshtml";
|
Layout = "_Layout.cshtml";
|
||||||
object areaObject;
|
string areaName;
|
||||||
ViewContext.ActionDescriptor.RouteValueDefaults.TryGetValue("area", out areaObject);
|
ViewContext.RouteData.Values.TryGetValue("area", out areaName);
|
||||||
var areaName = (areaObject as string) ?? "root";
|
areaName = areaName ?? "root";
|
||||||
ViewBag.Title = "Details in " + areaName + " area.";
|
ViewBag.Title = "Details in " + areaName + " area.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue