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>();
HttpMethods = new List<string>();
Parameters = new List<ParameterModel>();
RouteConstraints = new List<IRouteConstraintProvider>();
}
public ActionModel([NotNull] ActionModel other)
@ -40,6 +41,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
// 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);
if (other.AttributeRouteModel != null)
{
@ -62,6 +64,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
/// </remarks>
public ApiExplorerModel ApiExplorer { get; set; }
public AttributeRouteModel AttributeRouteModel { get; set; }
public IReadOnlyList<object> Attributes { get; }
public ControllerModel Controller { get; set; }
@ -74,6 +78,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
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.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Routing;
@ -336,7 +337,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
var message = Resources.FormatAttributeRoute_TokenReplacement_ReplacementValueNotFound(
template,
token,
string.Join(", ", values.Keys));
string.Join(", ", values.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)));
throw new InvalidOperationException(message);
}

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
AttributeRoutes = new List<AttributeRouteModel>();
ActionConstraints = new List<IActionConstraintMetadata>();
Filters = new List<IFilter>();
RouteConstraints = new List<RouteConstraintAttribute>();
RouteConstraints = new List<IRouteConstraintProvider>();
}
public ControllerModel([NotNull] ControllerModel other)
@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
ActionConstraints = new List<IActionConstraintMetadata>(other.ActionConstraints);
Attributes = new List<object>(other.Attributes);
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.
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<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)
.Distinct());
actionModel.RouteConstraints.AddRange(attributes.OfType<IRouteConstraintProvider>());
var routeTemplateProvider =
attributes
.OfType<IRouteTemplateProvider>()

View File

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

View File

@ -56,11 +56,7 @@ namespace Microsoft.AspNet.Mvc
actionDescriptor.ControllerTypeInfo = controller.ControllerType;
AddApiExplorerInfo(actionDescriptor, action, controller);
AddRouteConstraints(actionDescriptor, controller, action);
AddControllerRouteConstraints(
actionDescriptor,
controller.RouteConstraints,
removalConstraints);
AddRouteConstraints(removalConstraints, actionDescriptor, controller, action);
if (IsAttributeRoutedAction(actionDescriptor))
{
@ -371,39 +367,17 @@ namespace Microsoft.AspNet.Mvc
}
public static void AddRouteConstraints(
ISet<string> removalConstraints,
ControllerActionDescriptor actionDescriptor,
ControllerModel controller,
ActionModel action)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"controller",
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
// Apply all the constraints defined on the action, then controller (for example, [Area])
// to the actions. 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
// controller should not match when a value has been given for area when matching a url or
// generating a link.
foreach (var constraintAttribute in routeconstraints)
foreach (var constraintAttribute in action.RouteConstraints)
{
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)

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;
ApiExplorer = new ApiExplorerModelValues(inner.ApiExplorer);
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();
if (inner.AttributeRouteModel != null)
{
@ -60,6 +62,12 @@ namespace Microsoft.AspNet.Mvc.Logging
/// See <see cref="ActionModel.Filters"/>.
/// </summary>
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>
/// 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();
ActionConstraints = inner.ActionConstraints?.Select(a => new ActionConstraintValues(a))?.ToList();
RouteConstraints = inner.RouteConstraints.Select(
r => new RouteConstraintAttributeValues(r)).ToList();
r => new RouteConstraintProviderValues(r)).ToList();
AttributeRoutes = inner.AttributeRoutes.Select(
a => new AttributeRouteModelValues(a)).ToList();
}
@ -72,10 +72,10 @@ namespace Microsoft.AspNet.Mvc.Logging
public List<ActionConstraintValues> ActionConstraints { get; }
/// <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"/>.
/// </summary>
public List<RouteConstraintAttributeValues> RouteConstraints { get; set; }
public List<RouteConstraintProviderValues> RouteConstraints { get; set; }
/// <summary>
/// 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
{
/// <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"/>
/// </summary>
public class RouteConstraintAttributeValues : LoggerStructureBase
public class RouteConstraintProviderValues : LoggerStructureBase
{
public RouteConstraintAttributeValues([NotNull] RouteConstraintAttribute inner)
public RouteConstraintProviderValues([NotNull] IRouteConstraintProvider inner)
{
RouteKey = inner.RouteKey;
RouteValue = inner.RouteValue;
@ -20,22 +20,22 @@ namespace Microsoft.AspNet.Mvc.Logging
}
/// <summary>
/// The route value key. See <see cref="RouteConstraintAttribute.RouteKey"/>.
/// The route value key. See <see cref="IRouteConstraintProvider.RouteKey"/>.
/// </summary>
public string RouteKey { get; }
/// <summary>
/// The expected route value. See <see cref="RouteConstraintAttribute.RouteValue"/>.
/// The expected route value. See <see cref="IRouteConstraintProvider.RouteValue"/>.
/// </summary>
public string RouteValue { get; }
/// <summary>
/// The <see cref="RouteKeyHandling"/>. See <see cref="RouteConstraintAttribute.RouteKeyHandling"/>.
/// The <see cref="RouteKeyHandling"/>. See <see cref="IRouteConstraintProvider.RouteKeyHandling"/>.
/// </summary>
public RouteKeyHandling RouteKeyHandling { get; }
/// <summary>
/// See <see cref="RouteConstraintAttribute.BlockNonAttributedActions"/>.
/// See <see cref="IRouteConstraintProvider.BlockNonAttributedActions"/>.
/// </summary>
public bool BlockNonAttributedActions { get; }
@ -44,4 +44,4 @@ namespace Microsoft.AspNet.Mvc.Logging
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
/// actions defined by the controller.
///
///
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class RouteConstraintAttribute : Attribute
public abstract class RouteConstraintAttribute : Attribute, IRouteConstraintProvider
{
/// <summary>
/// Creates a new <see cref="RouteConstraintAttribute"/>.
@ -65,25 +63,16 @@ namespace Microsoft.AspNet.Mvc
BlockNonAttributedActions = blockNonAttributedActions;
}
/// <summary>
/// The route value key.
/// </summary>
/// <inheritdoc />
public string RouteKey { get; private set; }
/// <summary>
/// The <see cref="RouteKeyHandling"/>.
/// </summary>
/// <inheritdoc />
public RouteKeyHandling RouteKeyHandling { get; private set; }
/// <summary>
/// The expected route value. Will be null unless <see cref="RouteConstraintAttribute.RouteKeyHandling"/> is
/// set to <see cref="RouteKeyHandling.RequireKey"/>.
/// </summary>
/// <inheritdoc />
public string RouteValue { get; private set; }
/// <summary>
/// Set to true to negate this constraint on all actions that do not define a behavior for this route key.
/// </summary>
/// <inheritdoc />
public bool BlockNonAttributedActions { get; private set; }
}
}

View File

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

View File

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

View File

@ -547,7 +547,7 @@ namespace Microsoft.AspNet.Mvc.Test
"For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" +
"MultipleErrorsController.Unknown'" + Environment.NewLine +
"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 +
"Error 2:" + Environment.NewLine +
"For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" +

View File

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