diff --git a/src/Microsoft.AspNet.Mvc.Core/AcceptVerbsAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/AcceptVerbsAttribute.cs
index 3c8e7db900..f4ddcfa148 100644
--- a/src/Microsoft.AspNet.Mvc.Core/AcceptVerbsAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/AcceptVerbsAttribute.cs
@@ -4,16 +4,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
///
/// Specifies what HTTP methods an action supports.
///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
- public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
{
private readonly IEnumerable _httpMethods;
+ private int? _order;
///
/// Initializes a new instance of the class.
@@ -45,5 +47,40 @@ namespace Microsoft.AspNet.Mvc
return _httpMethods;
}
}
+
+ ///
+ /// The route template. May be null.
+ ///
+ public string Route { get; set; }
+
+ ///
+ string IRouteTemplateProvider.Template
+ {
+ get { return Route; }
+ }
+
+ ///
+ /// Gets the route order. The order determines the order of route execution. Routes with a lower
+ /// order value are tried first. When a route doesn't specify a value, it gets the value of the
+ /// or a default value of 0 if the
+ /// doesn't define a value on the controller.
+ ///
+ public int Order
+ {
+ get { return _order ?? 0; }
+ set { _order = value; }
+ }
+
+ ///
+ int? IRouteTemplateProvider.Order
+ {
+ get
+ {
+ return _order;
+ }
+ }
+
+ ///
+ public string Name { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionConvention.cs b/src/Microsoft.AspNet.Mvc.Core/ActionConvention.cs
index ad733671e5..d8a57336bd 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ActionConvention.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionConvention.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
+using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
@@ -9,6 +10,7 @@ namespace Microsoft.AspNet.Mvc
{
public string ActionName { get; set; }
public string[] HttpMethods { get; set; }
+ public IRouteTemplateProvider AttributeRoute { get; set; }
public bool RequireActionNameMatch { get; set; }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs
index c12e93bc0f..0713d78cf8 100644
--- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
+using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
@@ -55,7 +56,7 @@ namespace Microsoft.AspNet.Mvc
// If the convention is All methods starting with Get do not have an action name,
// for a input GetXYZ methodInfo, the return value will be
- // { { HttpMethods = "GET", ActionName = "GetXYZ", RequireActionNameMatch = false }}
+ // { { HttpMethods = "GET", ActionName = "GetXYZ", RequireActionNameMatch = false, AttributeRoute = null }}
public virtual IEnumerable GetActions(
[NotNull] MethodInfo methodInfo,
[NotNull] TypeInfo controllerTypeInfo)
@@ -65,7 +66,7 @@ namespace Microsoft.AspNet.Mvc
return null;
}
- var actionInfos = GetActionsForMethodsWithCustomAttributes(methodInfo);
+ var actionInfos = GetActionsForMethodsWithCustomAttributes(methodInfo, controllerTypeInfo);
if (actionInfos.Any())
{
return actionInfos;
@@ -129,26 +130,77 @@ namespace Microsoft.AspNet.Mvc
var attributes = methodInfo.GetCustomAttributes();
var actionNameAttribute = attributes.OfType().FirstOrDefault();
var httpMethodConstraints = attributes.OfType();
+ var routeTemplates = attributes.OfType();
+
return new ActionAttributes()
{
+ ActionNameAttribute = actionNameAttribute,
HttpMethodProviderAttributes = httpMethodConstraints,
- ActionNameAttribute = actionNameAttribute
+ RouteTemplateProviderAttributes = routeTemplates,
};
}
- private IEnumerable GetActionsForMethodsWithCustomAttributes(MethodInfo methodInfo)
+ private IEnumerable GetActionsForMethodsWithCustomAttributes(
+ MethodInfo methodInfo,
+ TypeInfo controller)
{
+ var hasControllerAttributeRoutes = HasValidControllerRouteTemplates(controller);
var actionAttributes = GetActionCustomAttributes(methodInfo);
- if (!actionAttributes.Any())
+
+ // We need to check controllerRouteTemplates to take into account the
+ // case where the controller has [Route] on it and the action does not have any
+ // attributes applied to it.
+ if (actionAttributes.Any() || hasControllerAttributeRoutes)
+ {
+ var actionNameAttribute = actionAttributes.ActionNameAttribute;
+ var actionName = actionNameAttribute != null ? actionNameAttribute.Name : methodInfo.Name;
+
+ // The moment we see a non null attribute route template in the method or
+ // in the controller we consider the whole group to be attribute routed actions.
+ // If a combination ends up producing a non attribute routed action we consider
+ // that an error and throw at a later point in the pipeline.
+ if (hasControllerAttributeRoutes || ActionHasAttributeRoutes(actionAttributes))
+ {
+ return GetAttributeRoutedActions(actionAttributes, actionName);
+ }
+ else
+ {
+ return GetHttpConstrainedActions(actionAttributes, actionName);
+ }
+ }
+ else
{
// If the action is not decorated with any of the attributes,
// it would be handled by convention.
- yield break;
+ return Enumerable.Empty();
}
+ }
- var actionNameAttribute = actionAttributes.ActionNameAttribute;
- var actionName = actionNameAttribute != null ? actionNameAttribute.Name : methodInfo.Name;
+ private static bool ActionHasAttributeRoutes(ActionAttributes actionAttributes)
+ {
+ // We neet to check for null as some attributes implement IActionHttpMethodProvider
+ // and IRouteTemplateProvider and allow the user to provide a null template. An example
+ // of this is HttpGetAttribute. If the user provides a template, the attribute marks the
+ // action as attribute routed, but in other case, the attribute only adds a constraint
+ // that allows the action to be called with the GET HTTP method.
+ return actionAttributes.RouteTemplateProviderAttributes
+ .Any(rtp => rtp.Template != null);
+ }
+ private static bool HasValidControllerRouteTemplates(TypeInfo controller)
+ {
+ // A method inside a controller is considered to create attribute routed actions if the controller
+ // has one or more attributes that implement IRouteTemplateProvider with a non null template applied
+ // to it.
+ return controller.GetCustomAttributes()
+ .OfType()
+ .Any(cr => cr.Template != null);
+ }
+
+ private static IEnumerable GetHttpConstrainedActions(
+ ActionAttributes actionAttributes,
+ string actionName)
+ {
var httpMethodProviders = actionAttributes.HttpMethodProviderAttributes;
var httpMethods = httpMethodProviders.SelectMany(x => x.HttpMethods).Distinct().ToArray();
@@ -156,10 +208,54 @@ namespace Microsoft.AspNet.Mvc
{
HttpMethods = httpMethods,
ActionName = actionName,
- RequireActionNameMatch = true
+ RequireActionNameMatch = true,
};
}
+ private static IEnumerable GetAttributeRoutedActions(
+ ActionAttributes actionAttributes,
+ string actionName)
+ {
+ var actions = new List();
+
+ // This is the case where the controller has [Route] applied to it and
+ // the action doesn't have any [Route] or [Http*] attribute applied.
+ if (!actionAttributes.RouteTemplateProviderAttributes.Any())
+ {
+ actions.Add(new ActionInfo
+ {
+ ActionName = actionName,
+ HttpMethods = null,
+ RequireActionNameMatch = true,
+ AttributeRoute = null
+ });
+ }
+
+ foreach (var routeTemplateProvider in actionAttributes.RouteTemplateProviderAttributes)
+ {
+ actions.Add(new ActionInfo()
+ {
+ ActionName = actionName,
+ HttpMethods = GetRouteTemplateHttpMethods(routeTemplateProvider),
+ RequireActionNameMatch = true,
+ AttributeRoute = routeTemplateProvider
+ });
+ }
+
+ return actions;
+ }
+
+ private static string[] GetRouteTemplateHttpMethods(IRouteTemplateProvider routeTemplateProvider)
+ {
+ var provider = routeTemplateProvider as IActionHttpMethodProvider;
+ if (provider != null && provider.HttpMethods != null)
+ {
+ return provider.HttpMethods.ToArray();
+ }
+
+ return null;
+ }
+
private IEnumerable GetActionsForMethodsWithoutCustomAttributes(
MethodInfo methodInfo,
TypeInfo controllerTypeInfo)
@@ -226,12 +322,15 @@ namespace Microsoft.AspNet.Mvc
private class ActionAttributes
{
- public IEnumerable HttpMethodProviderAttributes { get; set; }
public ActionNameAttribute ActionNameAttribute { get; set; }
+ public IEnumerable HttpMethodProviderAttributes { get; set; }
+ public IEnumerable RouteTemplateProviderAttributes { get; set; }
public bool Any()
{
- return ActionNameAttribute != null || HttpMethodProviderAttributes.Any();
+ return ActionNameAttribute != null ||
+ HttpMethodProviderAttributes.Any() ||
+ RouteTemplateProviderAttributes.Any();
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
index 9fbdbc05fd..64f5913186 100644
--- a/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
@@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Mvc
///
/// Identifies an action that only supports a given set of HTTP methods.
///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class HttpMethodAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
{
private int? _order;
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index e444499ce1..3664b0dcd7 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -1354,6 +1354,86 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_AggregateErrorMessage_ErrorNumber"), p0, p1, p2);
}
+ ///
+ /// A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}
+ ///
+ internal static string AttributeRoute_InvalidHttpConstraints
+ {
+ get { return GetString("AttributeRoute_InvalidHttpConstraints"); }
+ }
+
+ ///
+ /// A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}
+ ///
+ internal static string FormatAttributeRoute_InvalidHttpConstraints(object p0, object p1, object p2, object p3, object p4)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_InvalidHttpConstraints"), p0, p1, p2, p3, p4);
+ }
+
+ ///
+ /// Action '{0}' with route template '{1}' has '{2}' invalid '{3}' attributes.
+ ///
+ internal static string AttributeRoute_InvalidHttpConstraints_Item
+ {
+ get { return GetString("AttributeRoute_InvalidHttpConstraints_Item"); }
+ }
+
+ ///
+ /// Action '{0}' with route template '{1}' has '{2}' invalid '{3}' attributes.
+ ///
+ internal static string FormatAttributeRoute_InvalidHttpConstraints_Item(object p0, object p1, object p2, object p3)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_InvalidHttpConstraints_Item"), p0, p1, p2, p3);
+ }
+
+ ///
+ /// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}
+ ///
+ internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod
+ {
+ get { return GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod"); }
+ }
+
+ ///
+ /// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}
+ ///
+ internal static string FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod"), p0, p1, p2);
+ }
+
+ ///
+ /// Action: '{0}' - Template: '{1}'
+ ///
+ internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item
+ {
+ get { return GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item"); }
+ }
+
+ ///
+ /// Action: '{0}' - Template: '{1}'
+ ///
+ internal static string FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item"), p0, p1);
+ }
+
+ ///
+ /// (none)
+ ///
+ internal static string AttributeRoute_NullTemplateRepresentation
+ {
+ get { return GetString("AttributeRoute_NullTemplateRepresentation"); }
+ }
+
+ ///
+ /// (none)
+ ///
+ internal static string FormatAttributeRoute_NullTemplateRepresentation()
+ {
+ return GetString("AttributeRoute_NullTemplateRepresentation");
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
index 704e0a8c17..0b08315ec9 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
@@ -4,9 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-#if ASPNETCORE50
using System.Reflection;
-#endif
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
using Microsoft.AspNet.Mvc.Routing;
@@ -99,6 +97,12 @@ namespace Microsoft.AspNet.Mvc
actionModel.IsActionNameMatchRequired = actionInfo.RequireActionNameMatch;
actionModel.HttpMethods.AddRange(actionInfo.HttpMethods ?? Enumerable.Empty());
+ if (actionInfo.AttributeRoute != null)
+ {
+ actionModel.AttributeRouteModel = new ReflectedAttributeRouteModel(
+ actionInfo.AttributeRoute);
+ }
+
foreach (var parameter in methodInfo.GetParameters())
{
actionModel.Parameters.Add(new ReflectedParameterModel(parameter));
@@ -119,48 +123,74 @@ namespace Microsoft.AspNet.Mvc
var hasAttributeRoutes = false;
var removalConstraints = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var methodInfoMap = new MethodToActionMap();
+
var routeTemplateErrors = new List();
+ var attributeRoutingConfigurationErrors = new Dictionary();
foreach (var controller in model.Controllers)
{
var controllerDescriptor = new ControllerDescriptor(controller.ControllerType);
foreach (var action in controller.Actions)
{
- var actionDescriptor = CreateActionDescriptor(
- action,
- controller,
- controllerDescriptor,
- model.Filters);
+ // Controllers with multiple [Route] attributes (or user defined implementation of
+ // IRouteTemplateProvider) will generate one action descriptor per IRouteTemplateProvider
+ // instance.
+ // Actions with multiple [Http*] attributes or other (IRouteTemplateProvider implementations
+ // have already been identified as different actions during action discovery.
+ var actionDescriptors = CreateActionDescriptors(action, controller, controllerDescriptor);
- AddActionConstraints(actionDescriptor, action, controller);
- AddControllerRouteConstraints(actionDescriptor, controller.RouteConstraints, removalConstraints);
-
- if (IsAttributeRoutedAction(actionDescriptor))
+ foreach (var actionDescriptor in actionDescriptors)
{
- hasAttributeRoutes = true;
+ AddActionFilters(actionDescriptor, action.Filters, controller.Filters, model.Filters);
+ AddActionConstraints(actionDescriptor, action, controller);
+ AddControllerRouteConstraints(
+ actionDescriptor,
+ controller.RouteConstraints,
+ removalConstraints);
- // An attribute routed action will ignore conventional routed constraints. We still
- // want to provide these values as ambient values for link generation.
- AddConstraintsAsDefaultRouteValues(actionDescriptor);
+ if (IsAttributeRoutedAction(actionDescriptor))
+ {
+ hasAttributeRoutes = true;
- // Replaces tokens like [controller]/[action] in the route template with the actual values
- // for this action.
- ReplaceAttributeRouteTokens(actionDescriptor, routeTemplateErrors);
+ // An attribute routed action will ignore conventional routed constraints. We still
+ // want to provide these values as ambient values for link generation.
+ AddConstraintsAsDefaultRouteValues(actionDescriptor);
- // Attribute routed actions will ignore conventional routed constraints. Instead they have
- // a single route constraint "RouteGroup" associated with it.
- ReplaceRouteConstraints(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);
+ }
}
- actions.Add(actionDescriptor);
+ methodInfoMap.AddToMethodInfo(action, actionDescriptors);
+ actions.AddRange(actionDescriptors);
}
}
var actionsByRouteName = new Dictionary>(
StringComparer.OrdinalIgnoreCase);
+ // Keeps track of all the methods that we've validated to avoid visiting each action group
+ // more than once.
+ var validatedMethods = new HashSet();
+
foreach (var actionDescriptor in actions)
{
+ if (!validatedMethods.Contains(actionDescriptor.MethodInfo))
+ {
+ ValidateActionGroupConfiguration(
+ methodInfoMap,
+ actionDescriptor,
+ attributeRoutingConfigurationErrors);
+
+ validatedMethods.Add(actionDescriptor.MethodInfo);
+ }
+
if (!IsAttributeRoutedAction(actionDescriptor))
{
// Any attribute routes are in use, then non-attribute-routed action descriptors can't be
@@ -203,34 +233,71 @@ namespace Microsoft.AspNet.Mvc
}
}
+ if (attributeRoutingConfigurationErrors.Any())
+ {
+ var message = CreateAttributeRoutingAggregateErrorMessage(
+ attributeRoutingConfigurationErrors.Values);
+
+ throw new InvalidOperationException(message);
+ }
+
var namedRoutedErrors = ValidateNamedAttributeRoutedActions(actionsByRouteName);
if (namedRoutedErrors.Any())
{
- namedRoutedErrors = AddErrorNumbers(namedRoutedErrors);
-
- var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
- Environment.NewLine,
- string.Join(Environment.NewLine + Environment.NewLine, namedRoutedErrors));
-
+ var message = CreateAttributeRoutingAggregateErrorMessage(namedRoutedErrors);
throw new InvalidOperationException(message);
}
if (routeTemplateErrors.Any())
{
- var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
- Environment.NewLine,
- string.Join(Environment.NewLine + Environment.NewLine, routeTemplateErrors));
-
+ var message = CreateAttributeRoutingAggregateErrorMessage(routeTemplateErrors);
throw new InvalidOperationException(message);
}
return actions;
}
- private static ReflectedActionDescriptor CreateActionDescriptor(ReflectedActionModel action,
+ private static IList CreateActionDescriptors(
+ ReflectedActionModel action,
ReflectedControllerModel controller,
- ControllerDescriptor controllerDescriptor,
- IEnumerable globalFilters)
+ ControllerDescriptor controllerDescriptor)
+ {
+ var actionDescriptors = new List();
+
+ // We check the action to see if the template allows combination behavior
+ // (It doesn't start with / or ~/) so that in the case where we have multiple
+ // [Route] attributes on the controller we don't end up creating multiple
+ // attribute identical attribute routes.
+ if (controller.AttributeRoutes != null &&
+ controller.AttributeRoutes.Count > 0 &&
+ (action.AttributeRouteModel == null ||
+ !action.AttributeRouteModel.IsAbsoluteTemplate))
+ {
+ foreach (var controllerAttributeRoute in controller.AttributeRoutes)
+ {
+ var actionDescriptor = CreateActionDescriptor(
+ action,
+ controllerAttributeRoute,
+ controllerDescriptor);
+
+ actionDescriptors.Add(actionDescriptor);
+ }
+ }
+ else
+ {
+ actionDescriptors.Add(CreateActionDescriptor(
+ action,
+ controllerAttributeRoute: null,
+ controllerDescriptor: controllerDescriptor));
+ }
+
+ return actionDescriptors;
+ }
+
+ private static ReflectedActionDescriptor CreateActionDescriptor(
+ ReflectedActionModel action,
+ ReflectedAttributeRouteModel controllerAttributeRoute,
+ ControllerDescriptor controllerDescriptor)
{
var parameterDescriptors = new List();
foreach (var parameter in action.Parameters)
@@ -257,7 +324,9 @@ namespace Microsoft.AspNet.Mvc
parameterDescriptors.Add(paramDescriptor);
}
- var attributeRouteInfo = CreateAttributeRouteInfo(action, controller);
+ var attributeRouteInfo = CreateAttributeRouteInfo(
+ action.AttributeRouteModel,
+ controllerAttributeRoute);
var actionDescriptor = new ReflectedActionDescriptor()
{
@@ -274,23 +343,30 @@ namespace Microsoft.AspNet.Mvc
action.ActionMethod.DeclaringType.FullName,
action.ActionMethod.Name);
- actionDescriptor.FilterDescriptors =
- action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
- .Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
- .Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
- .OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
- .ToList();
-
return actionDescriptor;
}
+ private static void AddActionFilters(
+ ReflectedActionDescriptor actionDescriptor,
+ IEnumerable actionFilters,
+ IEnumerable controllerFilters,
+ IEnumerable globalFilters)
+ {
+ actionDescriptor.FilterDescriptors = actionFilters
+ .Select(f => new FilterDescriptor(f, FilterScope.Action))
+ .Concat(controllerFilters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
+ .Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
+ .OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
+ .ToList();
+ }
+
private static AttributeRouteInfo CreateAttributeRouteInfo(
- ReflectedActionModel action,
- ReflectedControllerModel controller)
+ ReflectedAttributeRouteModel action,
+ ReflectedAttributeRouteModel controller)
{
var combinedRoute = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(
- controller.AttributeRouteModel,
- action.AttributeRouteModel);
+ controller,
+ action);
if (combinedRoute == null)
{
@@ -471,7 +547,7 @@ namespace Microsoft.AspNet.Mvc
}
private static IList AddErrorNumbers(
- IList namedRoutedErrors)
+ IEnumerable namedRoutedErrors)
{
return namedRoutedErrors
.Select((nre, i) =>
@@ -527,10 +603,240 @@ namespace Microsoft.AspNet.Mvc
return namedRouteErrors;
}
+ private void ValidateActionGroupConfiguration(
+ IDictionary>> methodMap,
+ ReflectedActionDescriptor actionDescriptor,
+ IDictionary routingConfigurationErrors)
+ {
+ string combinedErrorMessage = null;
+
+ var hasAttributeRoutedActions = false;
+ var hasConventionallyRoutedActions = false;
+
+ var invalidHttpMethodActions = new Dictionary>();
+
+ var actionsForMethod = methodMap[actionDescriptor.MethodInfo];
+ foreach (var reflectedAction in actionsForMethod)
+ {
+ foreach (var action in reflectedAction.Value)
+ {
+ if (IsAttributeRoutedAction(action))
+ {
+ hasAttributeRoutedActions = true;
+ }
+ else
+ {
+ hasConventionallyRoutedActions = true;
+ }
+ }
+
+ // Keep a list of actions with possible invalid IHttpActionMethodProvider attributes
+ // to generate an error in case the method generates attribute routed actions.
+ ValidateActionHttpMethodProviders(reflectedAction.Key, invalidHttpMethodActions);
+ }
+
+ // Validate that no method result in attribute and non attribute actions at the same time.
+ // By design, mixing attribute and conventionally actions in the same method is not allowed.
+ // This is for example the case when someone uses[HttpGet("Products")] and[HttpPost]
+ // on the same method.
+ if (hasAttributeRoutedActions && hasConventionallyRoutedActions)
+ {
+ combinedErrorMessage = CreateMixedRoutedActionDescriptorsErrorMessage(
+ actionDescriptor,
+ actionsForMethod);
+ }
+
+ // Validate that no method that creates attribute routed actions and
+ // also uses attributes that only constrain the set of HTTP methods. For example,
+ // if an attribute that implements IActionHttpMethodProvider but does not implement
+ // IRouteTemplateProvider is used with an attribute that implements IRouteTemplateProvider on
+ // the same action, the HTTP methods provided by the attribute that only implements
+ // IActionHttpMethodProvider would be silently ignored, so we choose to throw to
+ // inform the user of the invalid configuration.
+ if (hasAttributeRoutedActions && invalidHttpMethodActions.Any())
+ {
+ var errorMessage = CreateInvalidActionHttpMethodProviderErrorMessage(
+ actionDescriptor,
+ invalidHttpMethodActions,
+ actionsForMethod);
+
+ combinedErrorMessage = CombineErrorMessage(combinedErrorMessage, errorMessage);
+ }
+
+ if (combinedErrorMessage != null)
+ {
+ routingConfigurationErrors.Add(actionDescriptor.MethodInfo, combinedErrorMessage);
+ }
+ }
+
+ private static void ValidateActionHttpMethodProviders(
+ ReflectedActionModel reflectedAction,
+ IDictionary> invalidHttpMethodActions)
+ {
+ var invalidHttpMethodProviderAttributes = reflectedAction.Attributes
+ .Where(attr => attr is IActionHttpMethodProvider &&
+ !(attr is IRouteTemplateProvider))
+ .Select(attr => attr.GetType().FullName);
+
+ if (invalidHttpMethodProviderAttributes.Any())
+ {
+ invalidHttpMethodActions.Add(
+ reflectedAction,
+ invalidHttpMethodProviderAttributes);
+ }
+ }
+
+ private static string CombineErrorMessage(string combinedErrorMessage, string errorMessage)
+ {
+ if (combinedErrorMessage == null)
+ {
+ combinedErrorMessage = errorMessage;
+ }
+ else
+ {
+ combinedErrorMessage = string.Join(
+ Environment.NewLine,
+ combinedErrorMessage,
+ errorMessage);
+ }
+
+ return combinedErrorMessage;
+ }
+
+ private static string CreateInvalidActionHttpMethodProviderErrorMessage(
+ ReflectedActionDescriptor actionDescriptor,
+ IDictionary> invalidHttpMethodActions,
+ IDictionary> actionsForMethod)
+ {
+ var messagesForMethodInfo = new List();
+ foreach (var invalidAction in invalidHttpMethodActions)
+ {
+ var invalidAttributesList = string.Join(", ", invalidAction.Value);
+
+ foreach (var descriptor in actionsForMethod[invalidAction.Key])
+ {
+ // We only report errors in attribute routed actions. For example, an action
+ // that contains [HttpGet("Products")], [HttpPost] and [HttpHead], where [HttpHead]
+ // only implements IHttpActionMethodProvider and restricts the action to only allow
+ // the head method, will report that the action contains invalid IActionHttpMethodProvider
+ // attributes only for the action generated by [HttpGet("Products")].
+ // [HttpPost] will be treated as an action that produces a conventionally routed action
+ // and the fact that the method generates attribute and non attributed actions will be
+ // reported as a different error.
+ if (IsAttributeRoutedAction(descriptor))
+ {
+ var messageItem = Resources.FormatAttributeRoute_InvalidHttpConstraints_Item(
+ descriptor.DisplayName,
+ descriptor.AttributeRouteInfo.Template,
+ invalidAttributesList,
+ typeof(IActionHttpMethodProvider).FullName);
+
+ messagesForMethodInfo.Add(messageItem);
+ }
+ }
+ }
+
+ var methodFullName = string.Format("{0}.{1}",
+ actionDescriptor.MethodInfo.DeclaringType.FullName,
+ actionDescriptor.MethodInfo.Name);
+
+ // Sample message:
+ // A method 'MyApplication.CustomerController.Index' that defines attribute routed actions must
+ // not have attributes that implement 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider'
+ // and do not implement 'Microsoft.AspNet.Mvc.Routing.IRouteTemplateProvider':
+ // Action 'MyApplication.CustomerController.Index' has 'Namespace.CustomHttpMethodAttribute'
+ // invalid 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' attributes.
+ return
+ Resources.FormatAttributeRoute_InvalidHttpConstraints(
+ methodFullName,
+ typeof(IActionHttpMethodProvider).FullName,
+ typeof(IRouteTemplateProvider).FullName,
+ Environment.NewLine,
+ string.Join(Environment.NewLine, messagesForMethodInfo));
+ }
+
+ private static string CreateMixedRoutedActionDescriptorsErrorMessage(
+ ReflectedActionDescriptor actionDescriptor,
+ IDictionary> actionsForMethod)
+ {
+ // Text to show as the attribute route template for conventionally routed actions.
+ var nullTemplate = Resources.AttributeRoute_NullTemplateRepresentation;
+
+ var actionDescriptions = actionsForMethod
+ .SelectMany(a => a.Value)
+ .Select(ad =>
+ Resources.FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item(
+ ad.DisplayName,
+ ad.AttributeRouteInfo != null ? ad.AttributeRouteInfo.Template : nullTemplate));
+
+ var methodFullName = string.Format("{0}.{1}",
+ actionDescriptor.MethodInfo.DeclaringType.FullName,
+ actionDescriptor.MethodInfo.Name);
+
+ // Sample error message:
+ // A method 'MyApplication.CustomerController.Index' must not define attributed actions and
+ // non attributed actions at the same time:
+ // Action: 'MyApplication.CustomerController.Index' - Template: 'Products'
+ // Action: 'MyApplication.CustomerController.Index' - Template: '(none)'
+ return
+ Resources.FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod(
+ methodFullName,
+ Environment.NewLine,
+ string.Join(Environment.NewLine, actionDescriptions));
+ }
+
+ private static string CreateAttributeRoutingAggregateErrorMessage(
+ IEnumerable individualErrors)
+ {
+ var errorMessages = AddErrorNumbers(individualErrors);
+
+ var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
+ Environment.NewLine,
+ string.Join(Environment.NewLine + Environment.NewLine, errorMessages));
+ return message;
+ }
+
private static string GetRouteGroupValue(int order, string template)
{
var group = string.Format("{0}-{1}", order, template);
return ("__route__" + group).ToUpperInvariant();
}
+
+ // We need to build a map of methods to reflected actions and reflected actions to
+ // action descriptors so that we can validate later that no method produced attribute
+ // and non attributed actions at the same time, and that no method that produced attribute
+ // routed actions has no attributes that implement IActionHttpMethodProvider and do not
+ // implement IRouteTemplateProvider. For example:
+ //
+ // public class ProductsController
+ // {
+ // [HttpGet("Products")]
+ // [HttpPost]
+ // public ActionResult Items(){ ... }
+ //
+ // [HttpGet("Products")]
+ // [CustomHttpMethods("POST, PUT")]
+ // public ActionResult List(){ ... }
+ // }
+ private class MethodToActionMap :
+ Dictionary>>
+ {
+ public void AddToMethodInfo(ReflectedActionModel action,
+ IList actionDescriptors)
+ {
+ IDictionary> actionsForMethod = null;
+ if (TryGetValue(action.ActionMethod, out actionsForMethod))
+ {
+ actionsForMethod.Add(action, actionDescriptors);
+ }
+ else
+ {
+ var reflectedActionMap =
+ new Dictionary>();
+ reflectedActionMap.Add(action, actionDescriptors);
+ Add(action.ActionMethod, reflectedActionMap);
+ }
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs
index 41b186be57..944511ada4 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs
@@ -19,13 +19,6 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
Attributes = actionMethod.GetCustomAttributes(inherit: true).OfType