Streamlining action selection and route values

Removes a few concepts we don't need
Improves documentation
More things are IDictionary instead of other random types
This commit is contained in:
Ryan Nowak 2016-04-20 21:09:38 -07:00
parent 74a74fb3a8
commit af58c2e6b6
33 changed files with 420 additions and 679 deletions

View File

@ -3,15 +3,14 @@
using System;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Routing;
namespace ActionConstraintSample.Web
{
public class CountrySpecificAttribute : RouteConstraintAttribute, IActionConstraint
public class CountrySpecificAttribute : Attribute, IActionConstraint
{
private readonly string _countryCode;
public CountrySpecificAttribute(string countryCode)
: base("country", countryCode, blockNonAttributedActions: false)
{
_countryCode = countryCode;
}

View File

@ -5,10 +5,10 @@ using Microsoft.AspNetCore.Mvc.Routing;
namespace CustomRouteSample.Web
{
public class LocaleAttribute : RouteConstraintAttribute
public class LocaleAttribute : RouteValueAttribute
{
public LocaleAttribute(string locale)
: base("locale", routeValue: locale, blockNonAttributedActions: true)
: base("locale", routeValue: locale)
{
}
}

View File

@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Mvc.Routing;
namespace MvcSubAreaSample.Web
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class SubAreaAttribute : RouteConstraintAttribute
public class SubAreaAttribute : RouteValueAttribute
{
public SubAreaAttribute(string name)
: base("subarea", name, blockNonAttributedActions: true)
: base("subarea", name)
{
if (string.IsNullOrEmpty(name))
{

View File

@ -29,12 +29,11 @@ namespace MvcSubAreaSample.Web
public void PopulateValues(ViewLocationExpanderContext context)
{
var subArea = context.ActionContext.ActionDescriptor.RouteConstraints.FirstOrDefault(
s => s.RouteKey == "subarea" && !string.IsNullOrEmpty(s.RouteValue));
if (subArea != null)
string subArea;
if (context.ActionContext.ActionDescriptor.RouteValues.TryGetValue(_subAreaKey, out subArea) &&
!string.IsNullOrEmpty(subArea))
{
context.Values[_subAreaKey] = subArea.RouteValue;
context.Values[_subAreaKey] = subArea;
}
}
}

View File

@ -13,9 +13,10 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
{
public ActionDescriptor()
{
Properties = new Dictionary<object, object>();
RouteValueDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
Id = Guid.NewGuid().ToString();
Properties = new Dictionary<object, object>();
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
RouteValueDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
@ -25,7 +26,11 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
public virtual string Name { get; set; }
public IList<RouteDataActionConstraint> RouteConstraints { get; set; }
/// <summary>
/// Gets or sets the collection of route values that must be provided by routing
/// for the action to be selected.
/// </summary>
public IDictionary<string, string> RouteValues { get; set; }
public AttributeRouteInfo AttributeRouteInfo { get; set; }

View File

@ -1,63 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.Routing
{
/// <summary>
/// Constraints an action to a route key and value.
/// </summary>
public class RouteDataActionConstraint
{
private RouteDataActionConstraint(string routeKey)
{
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));
}
RouteKey = routeKey;
}
/// <summary>
/// Initializes a <see cref="RouteDataActionConstraint"/> with a key and value, that are
/// required to make the action match.
/// </summary>
/// <param name="routeKey">The route key.</param>
/// <param name="routeValue">The route value.</param>
/// <remarks>
/// Passing a <see cref="string.Empty"/> or <see langword="null" /> to <paramref name="routeValue"/>
/// is a way to express that routing cannot produce a value for this key.
/// </remarks>
public RouteDataActionConstraint(string routeKey, string routeValue)
: this(routeKey)
{
RouteValue = routeValue ?? string.Empty;
if (string.IsNullOrEmpty(routeValue))
{
KeyHandling = RouteKeyHandling.DenyKey;
}
else
{
KeyHandling = RouteKeyHandling.RequireKey;
}
}
/// <summary>
/// The route key this constraint matches against.
/// </summary>
public string RouteKey { get; private set; }
/// <summary>
/// The route value this constraint matches against.
/// </summary>
public string RouteValue { get; private set; }
/// <summary>
/// The key handling definition for this constraint.
/// </summary>
public RouteKeyHandling KeyHandling { get; private set; }
}
}

View File

@ -1,18 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Routing
{
public enum RouteKeyHandling
{
/// <summary>
/// Requires that the key will be in the route values, and that the content matches.
/// </summary>
RequireKey,
/// <summary>
/// Requires that the key will not be in the route values.
/// </summary>
DenyKey,
}
}

View File

@ -6,8 +6,9 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
Attributes = new List<object>(attributes);
Filters = new List<IFilterMetadata>();
Parameters = new List<ParameterModel>();
RouteConstraints = new List<IRouteConstraintProvider>();
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Properties = new Dictionary<object, object>();
Selectors = new List<SelectorModel>();
}
@ -56,11 +57,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
Attributes = new List<object>(other.Attributes);
Filters = new List<IFilterMetadata>(other.Filters);
Properties = new Dictionary<object, object>(other.Properties);
RouteValues = new Dictionary<string, string>(other.RouteValues, StringComparer.OrdinalIgnoreCase);
// Make a deep copy of other 'model' types.
ApiExplorer = new ApiExplorerModel(other.ApiExplorer);
Parameters = new List<ParameterModel>(other.Parameters.Select(p => new ParameterModel(p)));
RouteConstraints = new List<IRouteConstraintProvider>(other.RouteConstraints);
Selectors = new List<SelectorModel>(other.Selectors.Select(s => new SelectorModel(s)));
}
@ -88,7 +89,24 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public IList<ParameterModel> Parameters { get; }
public IList<IRouteConstraintProvider> RouteConstraints { get; }
/// <summary>
/// Gets a collection of route values that must be present in the
/// <see cref="RouteData.Values"/> for the corresponding action to be selected.
/// </summary>
/// <remarks>
/// <para>
/// The value of <see cref="ActionName"/> is considered an implicit route value corresponding
/// to the key <c>action</c> and the value of <see cref="ControllerModel.ControllerName"/> is
/// considered an implicit route value corresponding to the key <c>controller</c>. These entries
/// will be added to <see cref="ActionDescriptor.RouteValues"/>, but will not be visible in
/// <see cref="RouteValues"/>.
/// </para>
/// <para>
/// Entries in <see cref="RouteValues"/> can override entries in
/// <see cref="ControllerModel.RouteValues"/>.
/// </para>
/// </remarks>
public IDictionary<string, string> RouteValues { get; }
/// <summary>
/// Gets a set of properties associated with the action.

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
ControllerProperties = new List<PropertyModel>();
Filters = new List<IFilterMetadata>();
Properties = new Dictionary<object, object>();
RouteConstraints = new List<IRouteConstraintProvider>();
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Selectors = new List<SelectorModel>();
}
@ -56,7 +57,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
// These are just metadata, safe to create new collections
Attributes = new List<object>(other.Attributes);
Filters = new List<IFilterMetadata>(other.Filters);
RouteConstraints = new List<IRouteConstraintProvider>(other.RouteConstraints);
RouteValues = new Dictionary<string, string>(other.RouteValues, StringComparer.OrdinalIgnoreCase);
Properties = new Dictionary<object, object>(other.Properties);
// Make a deep copy of other 'model' types.
@ -97,7 +98,15 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public IList<IFilterMetadata> Filters { get; }
public IList<IRouteConstraintProvider> RouteConstraints { get; }
/// <summary>
/// Gets a collection of route values that must be present in the
/// <see cref="RouteData.Values"/> for the corresponding action to be selected.
/// </summary>
/// <remarks>
/// Entries in <see cref="RouteValues"/> can be overridden by entries in
/// <see cref="ActionModel.RouteValues"/>.
/// </remarks>
public IDictionary<string, string> RouteValues { get; }
/// <summary>
/// Gets a set of properties associated with the controller.

View File

@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Mvc.Routing;
namespace Microsoft.AspNetCore.Mvc
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AreaAttribute : RouteConstraintAttribute
public class AreaAttribute : RouteValueAttribute
{
public AreaAttribute(string areaName)
: base("area", areaName, blockNonAttributedActions: true)
: base("area", areaName)
{
if (string.IsNullOrEmpty(areaName))
{

View File

@ -83,36 +83,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
if (item.RouteConstraints != null)
if (item.RouteValues != null)
{
foreach (var constraint in item.RouteConstraints)
foreach (var kvp in item.RouteValues)
{
DecisionCriterionValue value;
if (constraint.KeyHandling == RouteKeyHandling.DenyKey)
{
// null and string.Empty are equivalent for route values, so just treat nulls as
// string.Empty.
value = new DecisionCriterionValue(value: string.Empty);
}
else if (constraint.KeyHandling == RouteKeyHandling.RequireKey)
{
value = new DecisionCriterionValue(value: constraint.RouteValue);
}
else
{
// We'd already have failed before getting here. The RouteDataActionConstraint constructor
// would throw.
#if NET451
throw new InvalidEnumArgumentException(
nameof(item),
(int)constraint.KeyHandling,
typeof(RouteKeyHandling));
#else
throw new ArgumentOutOfRangeException(nameof(item));
#endif
}
results.Add(constraint.RouteKey, value);
// null and string.Empty are equivalent for route values, so just treat nulls as
// string.Empty.
results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty));
}
}

View File

@ -48,14 +48,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
var tree = _decisionTreeProvider.DecisionTree;
var matchingRouteConstraints = tree.Select(context.RouteData.Values);
var matchingRouteValues = tree.Select(context.RouteData.Values);
var candidates = new List<ActionSelectorCandidate>();
// Perf: Avoid allocations
for (var i = 0; i < matchingRouteConstraints.Count; i++)
for (var i = 0; i < matchingRouteValues.Count; i++)
{
var action = matchingRouteConstraints[i];
var action = matchingRouteValues[i];
var constraints = _actionConstraintCache.GetActionConstraints(context.HttpContext, action);
candidates.Add(new ActionSelectorCandidate(action, constraints));
}

View File

@ -179,11 +179,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Dictionary<string, RouteTemplate> templateCache,
ActionDescriptor action)
{
var constraint = action.RouteConstraints
.FirstOrDefault(c => c.RouteKey == TreeRouter.RouteGroupKey);
if (constraint == null ||
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
constraint.RouteValue == null)
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
@ -196,7 +195,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var routeInfo = new RouteInfo()
{
ActionDescriptor = action,
RouteGroup = constraint.RouteValue,
RouteGroup = value,
};
try

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actions = new List<ControllerActionDescriptor>();
var hasAttributeRoutes = false;
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var routeValueKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var methodInfoMap = new MethodToActionMap();
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionDescriptor.ControllerTypeInfo = controller.ControllerType;
AddApiExplorerInfo(actionDescriptor, application, controller, action);
AddRouteConstraints(removalConstraints, actionDescriptor, controller, action);
AddRouteValues(routeValueKeys, actionDescriptor, controller, action);
AddProperties(actionDescriptor, action, controller, application);
actionDescriptor.BoundProperties = controllerPropertyDescriptors;
@ -77,15 +77,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// An attribute routed action will ignore conventional routed constraints. We still
// want to provide these values as ambient values for link generation.
AddConstraintsAsDefaultRouteValues(actionDescriptor);
AddRouteValuesAsDefaultRouteValues(actionDescriptor);
// Replaces tokens like [controller]/[action] in the route template with the actual values
// for this action.
ReplaceAttributeRouteTokens(actionDescriptor, routeTemplateErrors);
// Attribute routed actions will ignore conventional routed constraints. Instead they have
// a single route constraint "RouteGroup" associated with it.
ReplaceRouteConstraints(actionDescriptor);
// Attribute routed actions will ignore conventional routed values. Instead they have
// a single route value "RouteGroup" associated with it.
ReplaceRouteValues(actionDescriptor);
}
}
@ -119,16 +119,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// selected when a route group returned by the route.
if (hasAttributeRoutes)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
TreeRouter.RouteGroupKey,
string.Empty));
actionDescriptor.RouteValues.Add(TreeRouter.RouteGroupKey, string.Empty);
}
// Add a route constraint with DenyKey for each constraint in the set to all the
// actions that don't have that constraint. For example, if a controller defines
// an area constraint, all actions that don't belong to an area must have a route
// constraint that prevents them from matching an incoming request.
AddRemovalConstraints(actionDescriptor, removalConstraints);
// 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
{
@ -145,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
//
// Consider an action like { area = "", controller = "Home", action = "Index" }. Even if
// it's attribute routed, it needs to know that area must be null to generate a link.
foreach (var key in removalConstraints)
foreach (var key in routeValueKeys)
{
if (!actionDescriptor.RouteValueDefaults.ContainsKey(key))
{
@ -287,7 +285,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = action.ActionName,
MethodInfo = action.ActionMethod,
Parameters = parameterDescriptors,
RouteConstraints = new List<RouteDataActionConstraint>(),
AttributeRouteInfo = CreateAttributeRouteInfo(actionAttributeRoute, controllerAttributeRoute)
};
@ -448,8 +445,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
public static void AddRouteConstraints(
ISet<string> removalConstraints,
public static void AddRouteValues(
ISet<string> keys,
ControllerActionDescriptor actionDescriptor,
ControllerModel controller,
ActionModel action)
@ -459,72 +456,48 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// without the constraint to match. For example, actions without an [Area] attribute on their
// controller should not match when a value has been given for area when matching a url or
// generating a link.
foreach (var constraintAttribute in action.RouteConstraints)
foreach (var kvp in action.RouteValues)
{
if (constraintAttribute.BlockNonAttributedActions)
{
removalConstraints.Add(constraintAttribute.RouteKey);
}
keys.Add(kvp.Key);
// Skip duplicates
if (!HasConstraint(actionDescriptor.RouteConstraints, constraintAttribute.RouteKey))
if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteValue));
actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
}
}
foreach (var constraintAttribute in controller.RouteConstraints)
foreach (var kvp in controller.RouteValues)
{
if (constraintAttribute.BlockNonAttributedActions)
{
removalConstraints.Add(constraintAttribute.RouteKey);
}
keys.Add(kvp.Key);
// Skip duplicates - this also means that a value on the action will take precedence
if (!HasConstraint(actionDescriptor.RouteConstraints, constraintAttribute.RouteKey))
if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteValue));
actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
}
}
// Lastly add the 'default' values
if (!HasConstraint(actionDescriptor.RouteConstraints, "action"))
if (!actionDescriptor.RouteValues.ContainsKey("action"))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
action.ActionName ?? string.Empty));
actionDescriptor.RouteValues.Add("action", action.ActionName ?? string.Empty);
}
if (!HasConstraint(actionDescriptor.RouteConstraints, "controller"))
if (!actionDescriptor.RouteValues.ContainsKey("controller"))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"controller",
controller.ControllerName));
actionDescriptor.RouteValues.Add("controller", controller.ControllerName);
}
}
private static bool HasConstraint(IList<RouteDataActionConstraint> constraints, string routeKey)
{
return constraints.Any(
rc => string.Equals(rc.RouteKey, routeKey, StringComparison.OrdinalIgnoreCase));
}
private static void ReplaceRouteConstraints(ControllerActionDescriptor actionDescriptor)
private static void ReplaceRouteValues(ControllerActionDescriptor actionDescriptor)
{
var routeGroupValue = GetRouteGroupValue(
actionDescriptor.AttributeRouteInfo.Order,
actionDescriptor.AttributeRouteInfo.Template);
var routeConstraints = new List<RouteDataActionConstraint>();
routeConstraints.Add(new RouteDataActionConstraint(
TreeRouter.RouteGroupKey,
routeGroupValue));
actionDescriptor.RouteConstraints = routeConstraints;
actionDescriptor.RouteValues.Clear();
actionDescriptor.RouteValues.Add(TreeRouter.RouteGroupKey, routeGroupValue);
}
private static void ReplaceAttributeRouteTokens(
@ -557,31 +530,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
private static void AddConstraintsAsDefaultRouteValues(ControllerActionDescriptor actionDescriptor)
private static void AddRouteValuesAsDefaultRouteValues(ControllerActionDescriptor actionDescriptor)
{
foreach (var constraint in actionDescriptor.RouteConstraints)
foreach (var kvp in actionDescriptor.RouteValues)
{
// We don't need to do anything with attribute routing for 'catch all' behavior. Order
// and precedence of attribute routes allow this kind of behavior.
if (constraint.KeyHandling == RouteKeyHandling.RequireKey ||
constraint.KeyHandling == RouteKeyHandling.DenyKey)
{
actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
}
actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value);
}
}
private static void AddRemovalConstraints(
private static void AddGlobalRouteValues(
ControllerActionDescriptor actionDescriptor,
ISet<string> removalConstraints)
{
foreach (var key in removalConstraints)
{
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
if (!actionDescriptor.RouteValues.ContainsKey(key))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
key,
string.Empty));
actionDescriptor.RouteValues.Add(key, string.Empty);
}
}
}

View File

@ -170,7 +170,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
typeInfo.Name;
AddRange(controllerModel.Filters, attributes.OfType<IFilterMetadata>());
AddRange(controllerModel.RouteConstraints, attributes.OfType<IRouteConstraintProvider>());
foreach (var routeValueProvider in attributes.OfType<IRouteValueProvider>())
{
controllerModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue);
}
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiVisibility != null)
@ -285,8 +289,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionModel.ApiExplorer.GroupName = apiGroupName.GroupName;
}
AddRange(actionModel.RouteConstraints, attributes.OfType<IRouteConstraintProvider>());
foreach (var routeValueProvider in attributes.OfType<IRouteValueProvider>())
{
actionModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue);
}
//TODO: modify comment
// Now we need to determine the action selection info (cross-section of routes and constraints)
//

View File

@ -1,78 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.Routing
{
/// <summary>
/// An attribute which specifies a required route value for an action or controller.
///
/// When placed on an action, the route data of a request must match the expectations of the route
/// constraint in order for the action to be selected. See <see cref="RouteKeyHandling"/> for
/// the expectations that must be satisfied by the route data.
///
/// When placed on a controller, unless overridden by the action, the constraint applies to all
/// actions defined by the controller.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class RouteConstraintAttribute : Attribute, IRouteConstraintProvider
{
/// <summary>
/// Creates a new <see cref="RouteConstraintAttribute"/> with <see cref="RouteKeyHandling"/> set as
/// <see cref="RouteKeyHandling.DenyKey"/>.
/// </summary>
/// <param name="routeKey">The route value key.</param>
protected RouteConstraintAttribute(string routeKey)
{
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));
}
RouteKey = routeKey;
RouteKeyHandling = RouteKeyHandling.DenyKey;
}
/// <summary>
/// Creates a new <see cref="RouteConstraintAttribute"/> with
/// <see cref="RouteConstraintAttribute.RouteKeyHandling"/> set to <see cref="RouteKeyHandling.RequireKey"/>.
/// </summary>
/// <param name="routeKey">The route value key.</param>
/// <param name="routeValue">The expected route value.</param>
/// <param name="blockNonAttributedActions">
/// Set to true to negate this constraint on all actions that do not define a behavior for this route key.
/// </param>
protected RouteConstraintAttribute(
string routeKey,
string routeValue,
bool blockNonAttributedActions)
{
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));
}
if (routeValue == null)
{
throw new ArgumentNullException(nameof(routeValue));
}
RouteKey = routeKey;
RouteValue = routeValue;
BlockNonAttributedActions = blockNonAttributedActions;
}
/// <inheritdoc />
public string RouteKey { get; private set; }
/// <inheritdoc />
public RouteKeyHandling RouteKeyHandling { get; private set; }
/// <inheritdoc />
public string RouteValue { get; private set; }
/// <inheritdoc />
public bool BlockNonAttributedActions { get; private set; }
}
}

View File

@ -1,33 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Routing
{
/// <summary>
/// An interface for metadata which provides <see cref="RouteDataActionConstraint"/> values
/// for a controller or action.
/// </summary>
public interface IRouteConstraintProvider
{
/// <summary>
/// The route value key.
/// </summary>
string RouteKey { get; }
/// <summary>
/// The <see cref="RouteKeyHandling"/>.
/// </summary>
RouteKeyHandling RouteKeyHandling { get; }
/// <summary>
/// The expected route value. Will be null unless <see cref="RouteKeyHandling"/> is
/// set to <see cref="RouteKeyHandling.RequireKey"/>.
/// </summary>
string RouteValue { get; }
/// <summary>
/// Set to true to negate this constraint on all actions that do not define a behavior for this route key.
/// </summary>
bool BlockNonAttributedActions { get; }
}
}

View File

@ -0,0 +1,85 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
namespace Microsoft.AspNetCore.Mvc.Routing
{
/// <summary>
/// <para>
/// A metadata interface which specifies a route value which is required for the action selector to
/// choose an action. When applied to an action using attribute routing, the route value will be added
/// to the <see cref="RouteData.Values"/> when the action is selected.
/// </para>
/// <para>
/// When an <see cref="IRouteValueProvider"/> is used to provide a new route value to an action, all
/// actions in the application must also have a value associated with that key, or have an implicit value
/// of <c>null</c>. See remarks for more details.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// The typical scheme for action selection in an MVC application is that an action will require the
/// matching values for its <see cref="ControllerActionDescriptor.ControllerName"/> and
/// <see cref="ActionDescriptor.Name"/>
/// </para>
/// <example>
/// For an action like <code>MyApp.Controllers.HomeController.Index()</code>, in order to be selected, the
/// <see cref="RouteData.Values"/> must contain the values
/// {
/// "action": "Index",
/// "controller": "Home"
/// }
/// </example>
/// <para>
/// If areas are in use in the application (see <see cref="AreaAttribute"/> which implements
/// <see cref="IRouteValueProvider"/>) then all actions are consider either in an area by having a
/// non-<c>null</c> area value (specified by <see cref="AreaAttribute"/> or another
/// <see cref="IRouteValueProvider"/>) or are considered 'outside' of areas by having the value <c>null</c>.
/// </para>
/// <example>
/// Consider an application with two controllers, each with an <code>Index</code> action method:
/// - <code>MyApp.Controllers.HomeController.Index()</code>
/// - <code>MyApp.Areas.Blog.Controllers.HomeController.Index()</code>
/// where <code>MyApp.Areas.Blog.Controllers.HomeController</code> has an area attribute
/// <code>[Area("Blog")]</code>.
///
/// For <see cref="RouteData.Values"/> like:
/// {
/// "action": "Index",
/// "controller": "Home"
/// }
///
/// <code>MyApp.Controllers.HomeController.Index()</code> will be selected.
/// <code>MyApp.Area.Blog.Controllers.HomeController.Index()</code> is not considered eligible because the
/// <see cref="RouteData.Values"/> does not contain the value 'Blog' for 'area'.
///
/// For <see cref="RouteData.Values"/> like:
/// {
/// "area": "Blog",
/// "action": "Index",
/// "controller": "Home"
/// }
///
/// <code>MyApp.Area.Blog.Controllers.HomeController.Index()</code> will be selected.
/// <code>MyApp.Controllers.HomeController.Index()</code> is not considered eligible because the route values
/// contain a value for 'area'. <code>MyApp.Controllers.HomeController.Index()</code> cannot match any value
/// for 'area' other than <c>null</c>.
/// </example>
/// </remarks>
public interface IRouteValueProvider
{
/// <summary>
/// The route value key.
/// </summary>
string RouteKey { get; }
/// <summary>
/// The route value. If <c>null</c> or empty, requires the route value associated with <see cref="RouteKey"/>
/// to be missing or <c>null</c>.
/// </summary>
string RouteValue { get; }
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
@ -71,18 +72,20 @@ namespace Microsoft.AspNetCore.Mvc.Routing
if (valuesCollection == null ||
version != valuesCollection.Version)
{
var routeValueCollection =
actionDescriptors
.Items
.Select(ad => ad.RouteConstraints.FirstOrDefault(
c => c.RouteKey == routeKey &&
c.KeyHandling == RouteKeyHandling.RequireKey))
.Where(rc => rc != null)
.Select(rc => rc.RouteValue)
.Distinct()
.ToArray();
var values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < actionDescriptors.Items.Count; i++)
{
var action = actionDescriptors.Items[i];
valuesCollection = new RouteValuesCollection(version, routeValueCollection);
string value;
if (action.RouteValues.TryGetValue(routeKey, out value) &&
!string.IsNullOrEmpty(value))
{
values.Add(value);
}
}
valuesCollection = new RouteValuesCollection(version, values.ToArray());
_cachedValuesCollection = valuesCollection;
}

View File

@ -0,0 +1,50 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.Routing
{
/// <summary>
/// An attribute which specifies a required route value for an action or controller.
///
/// When placed on an action, the route data of a request must match the expectations of the route
/// constraint in order for the action to be selected. See <see cref="IRouteValueProvider"/> for
/// the expectations that must be satisfied by the route data.
///
/// When placed on a controller, unless overridden by the action, the constraint applies to all
/// actions defined by the controller.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class RouteValueAttribute : Attribute, IRouteValueProvider
{
/// <summary>
/// Creates a new <see cref="RouteValueAttribute"/>.
/// </summary>
/// <param name="routeKey">The route value key.</param>
/// <param name="routeValue">The expected route value.</param>
protected RouteValueAttribute(
string routeKey,
string routeValue)
{
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));
}
if (routeValue == null)
{
throw new ArgumentNullException(nameof(routeValue));
}
RouteKey = routeKey;
RouteValue = routeValue;
}
/// <inheritdoc />
public string RouteKey { get; }
/// <inheritdoc />
public string RouteValue { get; }
}
}

View File

@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// 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
/// <see cref="Abstractions.ActionDescriptor.RouteValueDefaults"/> for attribute routes and
/// <see cref="Abstractions.ActionDescriptor.RouteConstraints"/> for traditional routes to get route values
/// <see cref="Abstractions.ActionDescriptor.RouteValues"/> for traditional routes to get route values
/// produces consistently cased results.
/// </remarks>
public static string GetNormalizedRouteValue(ActionContext context, string key)
@ -149,24 +149,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
else
{
// Perf: Avoid allocations
for (var i = 0; i < actionDescriptor.RouteConstraints.Count; i++)
string value;
if (actionDescriptor.RouteValues.TryGetValue(key, out value) &&
!string.IsNullOrEmpty(value))
{
var constraint = actionDescriptor.RouteConstraints[i];
if (string.Equals(constraint.RouteKey, key, StringComparison.Ordinal))
{
if (constraint.KeyHandling == RouteKeyHandling.DenyKey)
{
return null;
}
else
{
normalizedValue = constraint.RouteValue;
}
// Duplicate keys in RouteConstraints are not allowed.
break;
}
normalizedValue = value;
}
}

View File

@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
var namedAction = action;
var unnamedAction = new ActionModel(namedAction);
unnamedAction.RouteConstraints.Add(new UnnamedActionRouteConstraint());
unnamedAction.RouteValues.Add("action", null);
newActions.Add(unnamedAction);
}
}
@ -114,23 +114,5 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
actionSelectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { "POST" }));
}
}
private class UnnamedActionRouteConstraint : IRouteConstraintProvider
{
public UnnamedActionRouteConstraint()
{
RouteKey = "action";
RouteKeyHandling = RouteKeyHandling.DenyKey;
RouteValue = null;
}
public string RouteKey { get; }
public RouteKeyHandling RouteKeyHandling { get; }
public string RouteValue { get; }
public bool BlockNonAttributedActions { get; }
}
}
}

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
if (IsConventionApplicable(controller))
{
controller.RouteConstraints.Add(new AreaAttribute(_area));
controller.RouteValues.Add("area", _area);
}
}

View File

@ -64,10 +64,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
action.Selectors.Add(selectorModel);
action.ActionName = "Edit";
action.Controller = new ControllerModel(typeof(TestController).GetTypeInfo(),
new List<object>());
action.Controller = new ControllerModel
(typeof(TestController).GetTypeInfo(),
new List<object>());
action.Filters.Add(new MyFilterAttribute());
action.RouteConstraints.Add(new MyRouteConstraintAttribute());
action.RouteValues.Add("key", "value");
action.Properties.Add(new KeyValuePair<object, object>("test key", "test value"));
// Act
@ -95,6 +96,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
// Ensure non-default value
Assert.NotEmpty((IEnumerable<object>)value1);
}
else if (typeof(IDictionary<string, string>).IsAssignableFrom(property.PropertyType))
{
Assert.Equal(value1, value2);
// Ensure non-default value
Assert.NotEmpty((IDictionary<string, string>)value1);
}
else if (typeof(IDictionary<object, object>).IsAssignableFrom(property.PropertyType))
{
Assert.Equal(value1, value2);
@ -131,14 +139,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
}
private class MyRouteConstraintAttribute : Attribute, IRouteConstraintProvider
private class MyRouteValueAttribute : Attribute, IRouteValueProvider
{
public bool BlockNonAttributedActions { get { return true; } }
public string RouteKey { get; set; }
public RouteKeyHandling RouteKeyHandling { get { return RouteKeyHandling.RequireKey; } }
public string RouteValue { get; set; }
}
}

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
Assert.NotSame(controller.Actions, controller2.Actions);
Assert.NotSame(controller.Attributes, controller2.Attributes);
Assert.NotSame(controller.Filters, controller2.Filters);
Assert.NotSame(controller.RouteConstraints, controller2.RouteConstraints);
Assert.NotSame(controller.RouteValues, controller2.RouteValues);
}
[Fact]
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
controller.Application = new ApplicationModel();
controller.ControllerName = "cool";
controller.Filters.Add(new MyFilterAttribute());
controller.RouteConstraints.Add(new MyRouteConstraintAttribute());
controller.RouteValues.Add("key", "value");
controller.Properties.Add(new KeyValuePair<object, object>("test key", "test value"));
controller.ControllerProperties.Add(
new PropertyModel(typeof(TestController).GetProperty("TestProperty"), new List<object>()));
@ -99,6 +99,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
// Ensure non-default value
Assert.NotEmpty((IEnumerable<object>)value1);
}
else if (typeof(IDictionary<string, string>).IsAssignableFrom(property.PropertyType))
{
Assert.Equal(value1, value2);
// Ensure non-default value
Assert.NotEmpty((IDictionary<string, string>)value1);
}
else if (typeof(IDictionary<object, object>).IsAssignableFrom(property.PropertyType))
{
Assert.Equal(value1, value2);
@ -137,14 +144,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
}
private class MyRouteConstraintAttribute : Attribute, IRouteConstraintProvider
private class MyRouteValueAttribute : Attribute, IRouteValueProvider
{
public bool BlockNonAttributedActions { get { return true; } }
public string RouteKey { get; set; }
public RouteKeyHandling RouteKeyHandling { get { return RouteKeyHandling.RequireKey; } }
public string RouteValue { get; set; }
}
}

View File

@ -600,9 +600,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
return
actions
.Where(a => a.RouteConstraints.Any(c => c.RouteKey == "area" && comparer.Equals(c.RouteValue, area)))
.Where(a => a.RouteConstraints.Any(c => c.RouteKey == "controller" && comparer.Equals(c.RouteValue, controller)))
.Where(a => a.RouteConstraints.Any(c => c.RouteKey == "action" && comparer.Equals(c.RouteValue, action)));
.Where(a => a.RouteValues.Any(kvp => kvp.Key == "area" && comparer.Equals(kvp.Value, area)))
.Where(a => a.RouteValues.Any(kvp => kvp.Key == "controller" && comparer.Equals(kvp.Value, controller)))
.Where(a => a.RouteValues.Any(kvp => kvp.Key == "action" && comparer.Equals(kvp.Value, action)));
}
private static ActionSelector CreateSelector(IReadOnlyList<ActionDescriptor> actions, ILoggerFactory loggerFactory = null)
@ -667,24 +667,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var actionDescriptor = new ActionDescriptor()
{
Name = string.Format("Area: {0}, Controller: {1}, Action: {2}", area, controller, action),
RouteConstraints = new List<RouteDataActionConstraint>(),
Parameters = new List<ParameterDescriptor>(),
};
actionDescriptor.RouteConstraints.Add(
area == null ?
new RouteDataActionConstraint("area", null) :
new RouteDataActionConstraint("area", area));
actionDescriptor.RouteConstraints.Add(
controller == null ?
new RouteDataActionConstraint("controller", null) :
new RouteDataActionConstraint("controller", controller));
actionDescriptor.RouteConstraints.Add(
action == null ?
new RouteDataActionConstraint("action", null) :
new RouteDataActionConstraint("action", action));
actionDescriptor.RouteValues.Add("area", area);
actionDescriptor.RouteValues.Add("controller", controller);
actionDescriptor.RouteValues.Add("action", action);
return actionDescriptor;
}

View File

@ -1,41 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.Routing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
public class RouteDataActionConstraintTest
{
[Fact]
public void RouteDataActionConstraint_DenyKeyByPassingEmptyString()
{
var routeDataConstraint = new RouteDataActionConstraint("key", string.Empty);
Assert.Equal(routeDataConstraint.RouteKey, "key");
Assert.Equal(routeDataConstraint.KeyHandling, RouteKeyHandling.DenyKey);
Assert.Equal(routeDataConstraint.RouteValue, string.Empty);
}
[Fact]
public void RouteDataActionConstraint_DenyKeyByPassingNull()
{
var routeDataConstraint = new RouteDataActionConstraint("key", null);
Assert.Equal(routeDataConstraint.RouteKey, "key");
Assert.Equal(routeDataConstraint.KeyHandling, RouteKeyHandling.DenyKey);
Assert.Equal(routeDataConstraint.RouteValue, string.Empty);
}
[Fact]
public void RouteDataActionConstraint_RequireKeyByPassingNonEmpty()
{
var routeDataConstraint = new RouteDataActionConstraint("key", "value");
Assert.Equal(routeDataConstraint.RouteKey, "key");
Assert.Equal(routeDataConstraint.KeyHandling, RouteKeyHandling.RequireKey);
Assert.Equal(routeDataConstraint.RouteValue, "value");
}
}
}

View File

@ -42,9 +42,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
Template = "api/Blog/{id}"
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
},
new ActionDescriptor()
@ -53,9 +53,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
Template = "api/Store/Buy/{id}"
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "2"),
{ TreeRouter.RouteGroupKey, "2" }
},
},
};
@ -116,9 +116,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -164,9 +164,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -212,9 +212,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -263,9 +263,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -281,9 +281,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -339,9 +339,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -388,9 +388,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -437,9 +437,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -490,9 +490,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{
@ -508,9 +508,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Name = "BLOG_INDEX",
Order = 17,
},
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
{ TreeRouter.RouteGroupKey, "1" }
},
RouteValueDefaults = new Dictionary<string, object>()
{

View File

@ -131,9 +131,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var action = new ControllerActionDescriptor();
action.DisplayName = "Microsoft.AspNetCore.Mvc.Routing.AttributeRoutingTest+HomeController.Index";
action.MethodInfo = actionMethod;
action.RouteConstraints = new List<RouteDataActionConstraint>()
action.RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "group"),
{ TreeRouter.RouteGroupKey, "group" }
};
action.AttributeRouteInfo = new AttributeRouteInfo();
action.AttributeRouteInfo.Template = "{controller}/{action}";
@ -167,9 +167,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return new DisplayNameActionDescriptor()
{
DisplayName = displayName,
RouteConstraints = new List<RouteDataActionConstraint>()
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "whatever"),
{ TreeRouter.RouteGroupKey, "whatever" }
},
AttributeRouteInfo = new AttributeRouteInfo { Template = template },
};

View File

@ -242,17 +242,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var action = Assert.Single(descriptors);
Assert.NotNull(action.RouteConstraints);
Assert.NotNull(action.RouteValues);
var controller = Assert.Single(action.RouteConstraints,
rc => rc.RouteKey.Equals("controller"));
Assert.Equal(RouteKeyHandling.RequireKey, controller.KeyHandling);
Assert.Equal("ConventionallyRouted", controller.RouteValue);
var controller = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("controller"));
Assert.Equal("ConventionallyRouted", controller.Value);
var actionConstraint = Assert.Single(action.RouteConstraints,
rc => rc.RouteKey.Equals("action"));
Assert.Equal(RouteKeyHandling.RequireKey, actionConstraint.KeyHandling);
Assert.Equal(nameof(ConventionallyRoutedController.ConventionalAction), actionConstraint.RouteValue);
var actionConstraint = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("action"));
Assert.Equal(nameof(ConventionallyRoutedController.ConventionalAction), actionConstraint.Value);
}
[Fact]
@ -265,132 +261,67 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var action = Assert.Single(descriptors);
var routeconstraint = Assert.Single(action.RouteConstraints);
Assert.Equal(RouteKeyHandling.RequireKey, routeconstraint.KeyHandling);
Assert.Equal(TreeRouter.RouteGroupKey, routeconstraint.RouteKey);
Assert.Equal(TreeRouter.RouteGroupKey, Assert.Single(action.RouteValues).Key);
var controller = Assert.Single(action.RouteValueDefaults,
rc => rc.Key.Equals("controller"));
var controller = Assert.Single(action.RouteValueDefaults, kvp => kvp.Key.Equals("controller"));
Assert.Equal("AttributeRouted", controller.Value);
var actionConstraint = Assert.Single(action.RouteValueDefaults,
rc => rc.Key.Equals("action"));
var actionConstraint = Assert.Single(action.RouteValueDefaults, kvp => kvp.Key.Equals("action"));
Assert.Equal(nameof(AttributeRoutedController.AttributeRoutedAction), actionConstraint.Value);
}
[Fact]
public void GetDescriptors_WithRouteDataConstraint_WithBlockNonAttributedActions()
public void GetDescriptors_WithRouteValueAttribute()
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(HttpMethodController).GetTypeInfo(),
typeof(BlockNonAttributedActionsController).GetTypeInfo()).ToArray();
typeof(RouteValueController).GetTypeInfo()).ToArray();
var descriptorWithoutConstraint = Assert.Single(
var descriptorWithoutValue = Assert.Single(
descriptors,
ad => ad.RouteConstraints.Any(
c => c.RouteKey == "key" && c.KeyHandling == RouteKeyHandling.DenyKey));
ad => ad.RouteValues.Any(kvp => kvp.Key == "key" && string.IsNullOrEmpty(kvp.Value)));
var descriptorWithConstraint = Assert.Single(
var descriptorWithValue = Assert.Single(
descriptors,
ad => ad.RouteConstraints.Any(
c =>
c.KeyHandling == RouteKeyHandling.RequireKey &&
c.RouteKey == "key" &&
c.RouteValue == "value"));
ad => ad.RouteValues.Any(kvp => kvp.Key == "key" && kvp.Value == "value"));
// Assert
Assert.Equal(2, descriptors.Length);
Assert.Equal(3, descriptorWithConstraint.RouteConstraints.Count);
Assert.Equal(3, descriptorWithValue.RouteValues.Count);
Assert.Single(
descriptorWithConstraint.RouteConstraints,
descriptorWithValue.RouteValues,
c =>
c.RouteKey == "controller" &&
c.RouteValue == "BlockNonAttributedActions");
c.Key == "controller" &&
c.Value == "RouteValue");
Assert.Single(
descriptorWithConstraint.RouteConstraints,
descriptorWithValue.RouteValues,
c =>
c.RouteKey == "action" &&
c.RouteValue == "Edit");
c.Key == "action" &&
c.Value == "Edit");
Assert.Single(
descriptorWithConstraint.RouteConstraints,
descriptorWithValue.RouteValues,
c =>
c.RouteKey == "key" &&
c.RouteValue == "value" &&
c.KeyHandling == RouteKeyHandling.RequireKey);
c.Key == "key" &&
c.Value == "value");
Assert.Equal(3, descriptorWithoutConstraint.RouteConstraints.Count);
Assert.Equal(3, descriptorWithoutValue.RouteValues.Count);
Assert.Single(
descriptorWithoutConstraint.RouteConstraints,
descriptorWithoutValue.RouteValues,
c =>
c.RouteKey == "controller" &&
c.RouteValue == "HttpMethod");
c.Key == "controller" &&
c.Value == "HttpMethod");
Assert.Single(
descriptorWithoutConstraint.RouteConstraints,
descriptorWithoutValue.RouteValues,
c =>
c.RouteKey == "action" &&
c.RouteValue == "OnlyPost");
c.Key == "action" &&
c.Value == "OnlyPost");
Assert.Single(
descriptorWithoutConstraint.RouteConstraints,
descriptorWithoutValue.RouteValues,
c =>
c.RouteKey == "key" &&
c.RouteValue == string.Empty &&
c.KeyHandling == RouteKeyHandling.DenyKey);
}
[Fact]
public void GetDescriptors_WithRouteDataConstraint_WithoutBlockNonAttributedActions()
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(HttpMethodController).GetTypeInfo(),
typeof(DontBlockNonAttributedActionsController).GetTypeInfo()).ToArray();
var descriptorWithConstraint = Assert.Single(
descriptors,
ad => ad.RouteConstraints.Any(
c =>
c.KeyHandling == RouteKeyHandling.RequireKey &&
c.RouteKey == "key" &&
c.RouteValue == "value"));
var descriptorWithoutConstraint = Assert.Single(
descriptors,
ad => !ad.RouteConstraints.Any(c => c.RouteKey == "key"));
// Assert
Assert.Equal(2, descriptors.Length);
Assert.Equal(3, descriptorWithConstraint.RouteConstraints.Count);
Assert.Single(
descriptorWithConstraint.RouteConstraints,
c =>
c.RouteKey == "controller" &&
c.RouteValue == "DontBlockNonAttributedActions");
Assert.Single(
descriptorWithConstraint.RouteConstraints,
c =>
c.RouteKey == "action" &&
c.RouteValue == "Create");
Assert.Single(
descriptorWithConstraint.RouteConstraints,
c =>
c.RouteKey == "key" &&
c.RouteValue == "value" &&
c.KeyHandling == RouteKeyHandling.RequireKey);
Assert.Equal(2, descriptorWithoutConstraint.RouteConstraints.Count);
Assert.Single(
descriptorWithoutConstraint.RouteConstraints,
c =>
c.RouteKey == "controller" &&
c.RouteValue == "HttpMethod");
Assert.Single(
descriptorWithoutConstraint.RouteConstraints,
c =>
c.RouteKey == "action" &&
c.RouteValue == "OnlyPost");
c.Key == "key" &&
c.Value == string.Empty);
}
[Fact]
@ -1002,10 +933,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
foreach (var actionDescriptor in actionDescriptors.Where(ad => ad.AttributeRouteInfo == null))
{
Assert.Equal(6, actionDescriptor.RouteConstraints.Count);
var routeGroupConstraint = Assert.Single(actionDescriptor.RouteConstraints,
rc => rc.RouteKey.Equals(TreeRouter.RouteGroupKey));
Assert.Equal(RouteKeyHandling.DenyKey, routeGroupConstraint.KeyHandling);
Assert.Equal(6, actionDescriptor.RouteValues.Count);
Assert.Single(
actionDescriptor.RouteValues,
kvp => kvp.Key.Equals(TreeRouter.RouteGroupKey) && string.IsNullOrEmpty(kvp.Value));
}
}
@ -1026,11 +957,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var indexAction = Assert.Single(actionDescriptors, ad => ad.Name.Equals("Index"));
Assert.Equal(1, indexAction.RouteConstraints.Count);
Assert.Equal(1, indexAction.RouteValues.Count);
var routeGroupConstraint = Assert.Single(indexAction.RouteConstraints, rc => rc.RouteKey.Equals(TreeRouter.RouteGroupKey));
Assert.Equal(RouteKeyHandling.RequireKey, routeGroupConstraint.KeyHandling);
Assert.NotNull(routeGroupConstraint.RouteValue);
var routeGroup = Assert.Single(indexAction.RouteValues, kvp => kvp.Key.Equals(TreeRouter.RouteGroupKey));
Assert.NotNull(routeGroup.Value);
Assert.Equal(5, indexAction.RouteValueDefaults.Count);
@ -1043,11 +973,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var areaDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("area", StringComparison.OrdinalIgnoreCase));
Assert.Equal("Home", areaDefault.Value);
var myRouteConstraintDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("key", StringComparison.OrdinalIgnoreCase));
Assert.Null(myRouteConstraintDefault.Value);
var mvRouteValueDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("key", StringComparison.OrdinalIgnoreCase));
Assert.Null(mvRouteValueDefault.Value);
var anotherRouteConstraint = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("second", StringComparison.OrdinalIgnoreCase));
Assert.Null(anotherRouteConstraint.Value);
var anotherRouteValue = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("second", StringComparison.OrdinalIgnoreCase));
Assert.Null(anotherRouteValue.Value);
}
[Fact]
@ -1076,9 +1006,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actions = provider.GetDescriptors().ToArray();
var groupIds = actions.Select(
a => a.RouteConstraints
.Where(rc => rc.RouteKey == TreeRouter.RouteGroupKey)
.Select(rc => rc.RouteValue)
a => a.RouteValues
.Where(kvp => kvp.Key == TreeRouter.RouteGroupKey)
.Select(kvp => kvp.Value)
.Single())
.ToArray();
@ -1625,38 +1555,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ }
}
private class MyRouteConstraintAttribute : RouteConstraintAttribute
private class MyRouteValueAttribute : RouteValueAttribute
{
public MyRouteConstraintAttribute(bool blockNonAttributedActions)
: base("key", "value", blockNonAttributedActions)
public MyRouteValueAttribute()
: base("key", "value")
{
}
}
private class MySecondRouteConstraintAttribute : RouteConstraintAttribute
private class MySecondRouteValueAttribute : RouteValueAttribute
{
public MySecondRouteConstraintAttribute(bool blockNonAttributedActions)
: base("second", "value", blockNonAttributedActions)
public MySecondRouteValueAttribute()
: base("second", "value")
{
}
}
[MyRouteConstraint(blockNonAttributedActions: true)]
private class BlockNonAttributedActionsController
[MyRouteValue]
private class RouteValueController
{
public void Edit()
{
}
}
[MyRouteConstraint(blockNonAttributedActions: false)]
private class DontBlockNonAttributedActionsController
{
public void Create()
{
}
}
private class MyFilterAttribute : Attribute, IFilterMetadata
{
public MyFilterAttribute(int value)
@ -1677,7 +1599,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
[Route("api/Token/[key]/[controller]")]
[MyRouteConstraint(false)]
[MyRouteValue]
private class TokenReplacementController
{
[HttpGet("stub/[action]")]
@ -1880,8 +1802,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public void DifferentHttpMethods() { }
}
[MyRouteConstraint(blockNonAttributedActions: true)]
[MySecondRouteConstraint(blockNonAttributedActions: true)]
[MyRouteValue]
[MySecondRouteValue]
private class ConstrainedController
{
public void ConstrainedNonAttributedAction() { }

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var actionDescriptor = CreateActionDescriptor("testArea",
"testController",
"testAction");
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint("randomKey", "testRandom"));
actionDescriptor.RouteValues.Add("randomKey", "testRandom");
var httpContext = GetHttpContext(actionDescriptor);
var route = Mock.Of<IRouter>();
var values = new RouteValueDictionary()
@ -85,10 +85,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public void RouteValue_DoesNotExists_MatchFails(string keyName, RouteDirection direction)
{
// Arrange
var actionDescriptor = CreateActionDescriptor("testArea",
"testController",
"testAction");
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint("randomKey", "testRandom"));
var actionDescriptor = CreateActionDescriptor(
"testArea",
"testController",
"testAction");
actionDescriptor.RouteValues.Add("randomKey", "testRandom");
var httpContext = GetHttpContext(actionDescriptor);
var route = Mock.Of<IRouter>();
var values = new RouteValueDictionary()
@ -186,23 +187,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var actionDescriptor = new ActionDescriptor()
{
Name = string.Format("Area: {0}, Controller: {1}, Action: {2}", area, controller, action),
RouteConstraints = new List<RouteDataActionConstraint>(),
};
actionDescriptor.RouteConstraints.Add(
area == null ?
new RouteDataActionConstraint("area", null) :
new RouteDataActionConstraint("area", area));
actionDescriptor.RouteConstraints.Add(
controller == null ?
new RouteDataActionConstraint("controller", null) :
new RouteDataActionConstraint("controller", controller));
actionDescriptor.RouteConstraints.Add(
action == null ?
new RouteDataActionConstraint("action", null) :
new RouteDataActionConstraint("action", action));
actionDescriptor.RouteValues.Add("area", area);
actionDescriptor.RouteValues.Add("controller", controller);
actionDescriptor.RouteValues.Add("action", action);
return actionDescriptor;
}

View File

@ -1152,7 +1152,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
[Theory]
// Looks in RouteValueDefaults
[InlineData(true)]
// Looks in RouteConstraints
// Looks in RouteValues
[InlineData(false)]
public void FindPage_SelectsActionCaseInsensitively(bool isAttributeRouted)
{
@ -1194,7 +1194,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
[Theory]
// Looks in RouteValueDefaults
[InlineData(true)]
// Looks in RouteConstraints
// Looks in RouteValues
[InlineData(false)]
public void FindPage_LooksForPages_UsingActionDescriptor_Controller(bool isAttributeRouted)
{
@ -1232,7 +1232,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
[Theory]
// Looks in RouteValueDefaults
[InlineData(true)]
// Looks in RouteConstraints
// Looks in RouteValues
[InlineData(false)]
public void FindPage_LooksForPages_UsingActionDescriptor_Areas(bool isAttributeRouted)
{
@ -1495,17 +1495,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
}
[Fact]
public void GetNormalizedRouteValue_ReturnsValueFromRouteConstraints_IfKeyHandlingIsRequired()
public void GetNormalizedRouteValue_ReturnsValueFromRouteValues_IfKeyHandlingIsRequired()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
RouteConstraints = new[]
{
new RouteDataActionConstraint(key, "Route-Value")
}
};
var actionDescriptor = new ActionDescriptor();
actionDescriptor.RouteValues.Add(key, "Route-Value");
var actionContext = new ActionContext
{
@ -1523,17 +1518,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
}
[Fact]
public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatchRouteConstraint()
public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatch()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
RouteConstraints = new[]
{
new RouteDataActionConstraint(key, "different-value")
}
};
var actionDescriptor = new ActionDescriptor();
actionDescriptor.RouteValues.Add(key, "different-value");
var actionContext = new ActionContext
{
@ -1551,17 +1541,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
}
[Fact]
public void GetNormalizedRouteValue_ReturnsNull_IfRouteConstraintKeyHandlingIsDeny()
public void GetNormalizedRouteValue_ReturnsNonNormalizedValue_IfActionRouteValueIsNull()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
RouteConstraints = new[]
{
new RouteDataActionConstraint(key, routeValue: string.Empty)
}
};
var actionDescriptor = new ActionDescriptor();
actionDescriptor.RouteValues.Add(key, null);
var actionContext = new ActionContext
{
@ -1575,7 +1560,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Null(result);
Assert.Equal("route-value", result);
}
[Fact]
@ -1762,13 +1747,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
}
var actionDesciptor = new ActionDescriptor();
actionDesciptor.RouteConstraints = new List<RouteDataActionConstraint>();
return new ActionContext(httpContext, routeData, actionDesciptor);
}
private static ActionContext GetActionContextWithActionDescriptor(
IDictionary<string, object> routeValues,
IDictionary<string, string> routesInActionDescriptor,
IDictionary<string, string> actionRouteValues,
bool isAttributeRouted)
{
var httpContext = new DefaultHttpContext();
@ -1782,17 +1766,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
if (isAttributeRouted)
{
actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo();
foreach (var kvp in routesInActionDescriptor)
foreach (var kvp in actionRouteValues)
{
actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value);
}
}
else
{
actionDescriptor.RouteConstraints = new List<RouteDataActionConstraint>();
foreach (var kvp in routesInActionDescriptor)
foreach (var kvp in actionRouteValues)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value));
actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
}
}

View File

@ -84,14 +84,14 @@ namespace System.Web.Http
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "GetAll"));
a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "GetAll"));
Assert.Equal(
new string[] { "GET" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value)));
Assert.Equal(
new string[] { "GET" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
@ -120,14 +120,14 @@ namespace System.Web.Http
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "Edit"));
a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "Edit"));
Assert.Equal(
new string[] { "POST" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value)));
Assert.Equal(
new string[] { "POST" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
@ -156,14 +156,14 @@ namespace System.Web.Http
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "Delete"));
a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "Delete"));
Assert.Equal(
new string[] { "PUT" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value)));
Assert.Equal(
new string[] { "PUT" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
@ -193,14 +193,14 @@ namespace System.Web.Http
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "GetOptions"));
a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "GetOptions"));
Assert.Equal(
new string[] { "OPTIONS" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value)));
Assert.Equal(
new string[] { "OPTIONS" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodActionConstraint>()).HttpMethods);
@ -252,7 +252,7 @@ namespace System.Web.Http
Assert.NotEmpty(actions);
foreach (var action in actions)
{
Assert.Single(action.RouteConstraints, c => c.RouteKey == "area" && c.RouteValue == "api");
Assert.Single(action.RouteValues, c => c.Key == "area" && c.Value== "api");
}
}