Adding IRouteConstraintProvider and supporting it on actions

This change adds an interface for the functionality provide by
RouteConstraintAttribute, and adds support for configuration constraints
on actions/action-model.
This commit is contained in:
Ryan Nowak 2014-11-25 10:45:14 -08:00
parent 5dfd27e51f
commit ae9fc793ec
15 changed files with 131 additions and 70 deletions

View File

@ -20,6 +20,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
Filters = new List<IFilter>(); Filters = new List<IFilter>();
HttpMethods = new List<string>(); HttpMethods = new List<string>();
Parameters = new List<ParameterModel>(); Parameters = new List<ParameterModel>();
RouteConstraints = new List<IRouteConstraintProvider>();
} }
public ActionModel([NotNull] ActionModel other) public ActionModel([NotNull] ActionModel other)
@ -40,6 +41,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
// Make a deep copy of other 'model' types. // Make a deep copy of other 'model' types.
ApiExplorer = new ApiExplorerModel(other.ApiExplorer); ApiExplorer = new ApiExplorerModel(other.ApiExplorer);
Parameters = new List<ParameterModel>(other.Parameters.Select(p => new ParameterModel(p))); Parameters = new List<ParameterModel>(other.Parameters.Select(p => new ParameterModel(p)));
RouteConstraints = new List<IRouteConstraintProvider>(other.RouteConstraints);
if (other.AttributeRouteModel != null) if (other.AttributeRouteModel != null)
{ {
@ -62,6 +64,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
/// </remarks> /// </remarks>
public ApiExplorerModel ApiExplorer { get; set; } public ApiExplorerModel ApiExplorer { get; set; }
public AttributeRouteModel AttributeRouteModel { get; set; }
public IReadOnlyList<object> Attributes { get; } public IReadOnlyList<object> Attributes { get; }
public ControllerModel Controller { get; set; } public ControllerModel Controller { get; set; }
@ -74,6 +78,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
public List<ParameterModel> Parameters { get; private set; } public List<ParameterModel> Parameters { get; private set; }
public AttributeRouteModel AttributeRouteModel { get; set; } public List<IRouteConstraintProvider> RouteConstraints { get; private set; }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
@ -336,7 +337,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
var message = Resources.FormatAttributeRoute_TokenReplacement_ReplacementValueNotFound( var message = Resources.FormatAttributeRoute_TokenReplacement_ReplacementValueNotFound(
template, template,
token, token,
string.Join(", ", values.Keys)); string.Join(", ", values.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)));
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
AttributeRoutes = new List<AttributeRouteModel>(); AttributeRoutes = new List<AttributeRouteModel>();
ActionConstraints = new List<IActionConstraintMetadata>(); ActionConstraints = new List<IActionConstraintMetadata>();
Filters = new List<IFilter>(); Filters = new List<IFilter>();
RouteConstraints = new List<RouteConstraintAttribute>(); RouteConstraints = new List<IRouteConstraintProvider>();
} }
public ControllerModel([NotNull] ControllerModel other) public ControllerModel([NotNull] ControllerModel other)
@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
ActionConstraints = new List<IActionConstraintMetadata>(other.ActionConstraints); ActionConstraints = new List<IActionConstraintMetadata>(other.ActionConstraints);
Attributes = new List<object>(other.Attributes); Attributes = new List<object>(other.Attributes);
Filters = new List<IFilter>(other.Filters); Filters = new List<IFilter>(other.Filters);
RouteConstraints = new List<RouteConstraintAttribute>(other.RouteConstraints); RouteConstraints = new List<IRouteConstraintProvider>(other.RouteConstraints);
// Make a deep copy of other 'model' types. // Make a deep copy of other 'model' types.
Actions = new List<ActionModel>(other.Actions.Select(a => new ActionModel(a))); Actions = new List<ActionModel>(other.Actions.Select(a => new ActionModel(a)));
@ -65,6 +65,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
public List<IFilter> Filters { get; private set; } public List<IFilter> Filters { get; private set; }
public List<RouteConstraintAttribute> RouteConstraints { get; private set; } public List<IRouteConstraintProvider> RouteConstraints { get; private set; }
} }
} }

View File

@ -289,6 +289,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
.SelectMany(a => a.HttpMethods) .SelectMany(a => a.HttpMethods)
.Distinct()); .Distinct());
actionModel.RouteConstraints.AddRange(attributes.OfType<IRouteConstraintProvider>());
var routeTemplateProvider = var routeTemplateProvider =
attributes attributes
.OfType<IRouteTemplateProvider>() .OfType<IRouteTemplateProvider>()

View File

@ -122,7 +122,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
controllerModel.ActionConstraints.AddRange(attributes.OfType<IActionConstraintMetadata>()); controllerModel.ActionConstraints.AddRange(attributes.OfType<IActionConstraintMetadata>());
controllerModel.Filters.AddRange(attributes.OfType<IFilter>()); controllerModel.Filters.AddRange(attributes.OfType<IFilter>());
controllerModel.RouteConstraints.AddRange(attributes.OfType<RouteConstraintAttribute>()); controllerModel.RouteConstraints.AddRange(attributes.OfType<IRouteConstraintProvider>());
controllerModel.AttributeRoutes.AddRange( controllerModel.AttributeRoutes.AddRange(
attributes.OfType<IRouteTemplateProvider>().Select(rtp => new AttributeRouteModel(rtp))); attributes.OfType<IRouteTemplateProvider>().Select(rtp => new AttributeRouteModel(rtp)));
@ -142,4 +142,4 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
return controllerModel; return controllerModel;
} }
} }
} }

View File

@ -56,11 +56,7 @@ namespace Microsoft.AspNet.Mvc
actionDescriptor.ControllerTypeInfo = controller.ControllerType; actionDescriptor.ControllerTypeInfo = controller.ControllerType;
AddApiExplorerInfo(actionDescriptor, action, controller); AddApiExplorerInfo(actionDescriptor, action, controller);
AddRouteConstraints(actionDescriptor, controller, action); AddRouteConstraints(removalConstraints, actionDescriptor, controller, action);
AddControllerRouteConstraints(
actionDescriptor,
controller.RouteConstraints,
removalConstraints);
if (IsAttributeRoutedAction(actionDescriptor)) if (IsAttributeRoutedAction(actionDescriptor))
{ {
@ -371,39 +367,17 @@ namespace Microsoft.AspNet.Mvc
} }
public static void AddRouteConstraints( public static void AddRouteConstraints(
ISet<string> removalConstraints,
ControllerActionDescriptor actionDescriptor, ControllerActionDescriptor actionDescriptor,
ControllerModel controller, ControllerModel controller,
ActionModel action) ActionModel action)
{ {
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( // Apply all the constraints defined on the action, then controller (for example, [Area])
"controller", // to the actions. Also keep track of all the constraints that require preventing actions
controller.ControllerName));
if (action.IsActionNameMatchRequired)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
action.ActionName));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
string.Empty));
}
}
private static void AddControllerRouteConstraints(
ControllerActionDescriptor actionDescriptor,
IList<RouteConstraintAttribute> routeconstraints,
ISet<string> removalConstraints)
{
// Apply all the constraints defined on the controller (for example, [Area]) to the actions
// in that controller. Also keep track of all the constraints that require preventing actions
// without the constraint to match. For example, actions without an [Area] attribute on their // 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 // controller should not match when a value has been given for area when matching a url or
// generating a link. // generating a link.
foreach (var constraintAttribute in routeconstraints) foreach (var constraintAttribute in action.RouteConstraints)
{ {
if (constraintAttribute.BlockNonAttributedActions) if (constraintAttribute.BlockNonAttributedActions)
{ {
@ -427,6 +401,55 @@ namespace Microsoft.AspNet.Mvc
} }
} }
} }
foreach (var constraintAttribute in controller.RouteConstraints)
{
if (constraintAttribute.BlockNonAttributedActions)
{
removalConstraints.Add(constraintAttribute.RouteKey);
}
// Skip duplicates - this also means that a value on the action will take precedence
if (!HasConstraint(actionDescriptor.RouteConstraints, constraintAttribute.RouteKey))
{
if (constraintAttribute.RouteKeyHandling == RouteKeyHandling.CatchAll)
{
actionDescriptor.RouteConstraints.Add(
RouteDataActionConstraint.CreateCatchAll(
constraintAttribute.RouteKey));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteValue));
}
}
}
// Lastly add the 'default' values
if (!HasConstraint(actionDescriptor.RouteConstraints, "action"))
{
if (action.IsActionNameMatchRequired)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
action.ActionName));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
string.Empty));
}
}
if (!HasConstraint(actionDescriptor.RouteConstraints, "controller"))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"controller",
controller.ControllerName));
}
} }
private static bool HasConstraint(List<RouteDataActionConstraint> constraints, string routeKey) private static bool HasConstraint(List<RouteDataActionConstraint> constraints, string routeKey)

View File

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc
{
/// <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

@ -24,6 +24,8 @@ namespace Microsoft.AspNet.Mvc.Logging
ActionMethod = inner.ActionMethod; ActionMethod = inner.ActionMethod;
ApiExplorer = new ApiExplorerModelValues(inner.ApiExplorer); ApiExplorer = new ApiExplorerModelValues(inner.ApiExplorer);
Parameters = inner.Parameters.Select(p => new ParameterModelValues(p)).ToList(); Parameters = inner.Parameters.Select(p => new ParameterModelValues(p)).ToList();
RouteConstraints = inner.RouteConstraints.Select(
r => new RouteConstraintProviderValues(r)).ToList();
Filters = inner.Filters.Select(f => new FilterValues(f)).ToList(); Filters = inner.Filters.Select(f => new FilterValues(f)).ToList();
if (inner.AttributeRouteModel != null) if (inner.AttributeRouteModel != null)
{ {
@ -60,6 +62,12 @@ namespace Microsoft.AspNet.Mvc.Logging
/// See <see cref="ActionModel.Filters"/>. /// See <see cref="ActionModel.Filters"/>.
/// </summary> /// </summary>
public List<FilterValues> Filters { get; } public List<FilterValues> Filters { get; }
/// <summary>
/// The route constraints on the controller as <see cref="RouteConstraintProviderValues"/>.
/// See <see cref="ControllerModel.RouteConstraints"/>.
/// </summary>
public List<RouteConstraintProviderValues> RouteConstraints { get; set; }
/// <summary> /// <summary>
/// The attribute route model of the action as <see cref="AttributeRouteModelValues"/>. /// The attribute route model of the action as <see cref="AttributeRouteModelValues"/>.

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Mvc.Logging
Filters = inner.Filters.Select(f => new FilterValues(f)).ToList(); Filters = inner.Filters.Select(f => new FilterValues(f)).ToList();
ActionConstraints = inner.ActionConstraints?.Select(a => new ActionConstraintValues(a))?.ToList(); ActionConstraints = inner.ActionConstraints?.Select(a => new ActionConstraintValues(a))?.ToList();
RouteConstraints = inner.RouteConstraints.Select( RouteConstraints = inner.RouteConstraints.Select(
r => new RouteConstraintAttributeValues(r)).ToList(); r => new RouteConstraintProviderValues(r)).ToList();
AttributeRoutes = inner.AttributeRoutes.Select( AttributeRoutes = inner.AttributeRoutes.Select(
a => new AttributeRouteModelValues(a)).ToList(); a => new AttributeRouteModelValues(a)).ToList();
} }
@ -72,10 +72,10 @@ namespace Microsoft.AspNet.Mvc.Logging
public List<ActionConstraintValues> ActionConstraints { get; } public List<ActionConstraintValues> ActionConstraints { get; }
/// <summary> /// <summary>
/// The route constraints on the controller as <see cref="RouteConstraintAttributeValues"/>. /// The route constraints on the controller as <see cref="RouteConstraintProviderValues"/>.
/// See <see cref="ControllerModel.RouteConstraints"/>. /// See <see cref="ControllerModel.RouteConstraints"/>.
/// </summary> /// </summary>
public List<RouteConstraintAttributeValues> RouteConstraints { get; set; } public List<RouteConstraintProviderValues> RouteConstraints { get; set; }
/// <summary> /// <summary>
/// The attribute routes on the controller as <see cref="AttributeRouteModelValues"/>. /// The attribute routes on the controller as <see cref="AttributeRouteModelValues"/>.

View File

@ -6,12 +6,12 @@ using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc.Logging namespace Microsoft.AspNet.Mvc.Logging
{ {
/// <summary> /// <summary>
/// Logging representation of a <see cref="RouteConstraintAttribute"/>. Logged as a substructure of /// Logging representation of a <see cref="IRouteConstraintProvider"/>. Logged as a substructure of
/// <see cref="ControllerModelValues"/> /// <see cref="ControllerModelValues"/>
/// </summary> /// </summary>
public class RouteConstraintAttributeValues : LoggerStructureBase public class RouteConstraintProviderValues : LoggerStructureBase
{ {
public RouteConstraintAttributeValues([NotNull] RouteConstraintAttribute inner) public RouteConstraintProviderValues([NotNull] IRouteConstraintProvider inner)
{ {
RouteKey = inner.RouteKey; RouteKey = inner.RouteKey;
RouteValue = inner.RouteValue; RouteValue = inner.RouteValue;
@ -20,22 +20,22 @@ namespace Microsoft.AspNet.Mvc.Logging
} }
/// <summary> /// <summary>
/// The route value key. See <see cref="RouteConstraintAttribute.RouteKey"/>. /// The route value key. See <see cref="IRouteConstraintProvider.RouteKey"/>.
/// </summary> /// </summary>
public string RouteKey { get; } public string RouteKey { get; }
/// <summary> /// <summary>
/// The expected route value. See <see cref="RouteConstraintAttribute.RouteValue"/>. /// The expected route value. See <see cref="IRouteConstraintProvider.RouteValue"/>.
/// </summary> /// </summary>
public string RouteValue { get; } public string RouteValue { get; }
/// <summary> /// <summary>
/// The <see cref="RouteKeyHandling"/>. See <see cref="RouteConstraintAttribute.RouteKeyHandling"/>. /// The <see cref="RouteKeyHandling"/>. See <see cref="IRouteConstraintProvider.RouteKeyHandling"/>.
/// </summary> /// </summary>
public RouteKeyHandling RouteKeyHandling { get; } public RouteKeyHandling RouteKeyHandling { get; }
/// <summary> /// <summary>
/// See <see cref="RouteConstraintAttribute.BlockNonAttributedActions"/>. /// See <see cref="IRouteConstraintProvider.BlockNonAttributedActions"/>.
/// </summary> /// </summary>
public bool BlockNonAttributedActions { get; } public bool BlockNonAttributedActions { get; }
@ -44,4 +44,4 @@ namespace Microsoft.AspNet.Mvc.Logging
return LogFormatter.FormatStructure(this); return LogFormatter.FormatStructure(this);
} }
} }
} }

View File

@ -15,11 +15,9 @@ namespace Microsoft.AspNet.Mvc
/// ///
/// When placed on a controller, unless overridden by the action, the constraint applies to all /// When placed on a controller, unless overridden by the action, the constraint applies to all
/// actions defined by the controller. /// actions defined by the controller.
///
///
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class RouteConstraintAttribute : Attribute public abstract class RouteConstraintAttribute : Attribute, IRouteConstraintProvider
{ {
/// <summary> /// <summary>
/// Creates a new <see cref="RouteConstraintAttribute"/>. /// Creates a new <see cref="RouteConstraintAttribute"/>.
@ -65,25 +63,16 @@ namespace Microsoft.AspNet.Mvc
BlockNonAttributedActions = blockNonAttributedActions; BlockNonAttributedActions = blockNonAttributedActions;
} }
/// <summary> /// <inheritdoc />
/// The route value key.
/// </summary>
public string RouteKey { get; private set; } public string RouteKey { get; private set; }
/// <summary> /// <inheritdoc />
/// The <see cref="RouteKeyHandling"/>.
/// </summary>
public RouteKeyHandling RouteKeyHandling { get; private set; } public RouteKeyHandling RouteKeyHandling { get; private set; }
/// <summary> /// <inheritdoc />
/// The expected route value. Will be null unless <see cref="RouteConstraintAttribute.RouteKeyHandling"/> is
/// set to <see cref="RouteKeyHandling.RequireKey"/>.
/// </summary>
public string RouteValue { get; private set; } public string RouteValue { get; private set; }
/// <summary> /// <inheritdoc />
/// Set to true to negate this constraint on all actions that do not define a behavior for this route key.
/// </summary>
public bool BlockNonAttributedActions { get; private set; } public bool BlockNonAttributedActions { get; private set; }
} }
} }

View File

@ -53,6 +53,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
action.Filters.Add(new AuthorizeAttribute()); action.Filters.Add(new AuthorizeAttribute());
action.HttpMethods.Add("GET"); action.HttpMethods.Add("GET");
action.IsActionNameMatchRequired = true; action.IsActionNameMatchRequired = true;
action.RouteConstraints.Add(new AreaAttribute("Admin"));
// Act // Act
var action2 = new ActionModel(action); var action2 = new ActionModel(action);

View File

@ -200,7 +200,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
var expected = var expected =
"While processing template '[area]/[controller]/[action2]', " + "While processing template '[area]/[controller]/[action2]', " +
"a replacement value for the token 'action2' could not be found. " + "a replacement value for the token 'action2' could not be found. " +
"Available tokens: 'area, controller, action'."; "Available tokens: 'action, area, controller'.";
// Act // Act
var ex = Assert.Throws<InvalidOperationException>( var ex = Assert.Throws<InvalidOperationException>(

View File

@ -547,7 +547,7 @@ namespace Microsoft.AspNet.Mvc.Test
"For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" + "For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" +
"MultipleErrorsController.Unknown'" + Environment.NewLine + "MultipleErrorsController.Unknown'" + Environment.NewLine +
"Error: While processing template 'stub/[action]/[unknown]', a replacement value for the token 'unknown' " + "Error: While processing template 'stub/[action]/[unknown]', a replacement value for the token 'unknown' " +
"could not be found. Available tokens: 'controller, action'." + Environment.NewLine + "could not be found. Available tokens: 'action, controller'." + Environment.NewLine +
Environment.NewLine + Environment.NewLine +
"Error 2:" + Environment.NewLine + "Error 2:" + Environment.NewLine +
"For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" + "For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" +

View File

@ -5,10 +5,10 @@ using Xunit;
namespace Microsoft.AspNet.Mvc.Logging namespace Microsoft.AspNet.Mvc.Logging
{ {
public class RouteConstraintAttributeValuesTest public class RouteConstraintProviderValuesTest
{ {
[Fact] [Fact]
public void RouteConstraintAttributeValues_IncludesAllProperties() public void RouteConstraintProviderValues_IncludesAllProperties()
{ {
// Arrange // Arrange
var exclude = new[] { "TypeId" }; var exclude = new[] { "TypeId" };
@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Logging
// Assert // Assert
PropertiesAssert.PropertiesAreTheSame( PropertiesAssert.PropertiesAreTheSame(
typeof(RouteConstraintAttribute), typeof(RouteConstraintAttribute),
typeof(RouteConstraintAttributeValues), typeof(RouteConstraintProviderValues),
exclude); exclude);
} }
} }