diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionInfo.cs b/src/Microsoft.AspNet.Mvc.Core/ActionInfo.cs
deleted file mode 100644
index f3354530fd..0000000000
--- a/src/Microsoft.AspNet.Mvc.Core/ActionInfo.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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.
-
-using Microsoft.AspNet.Mvc.Routing;
-
-namespace Microsoft.AspNet.Mvc
-{
- public class ActionInfo
- {
- public string ActionName { get; set; }
-
- public string[] HttpMethods { get; set; }
-
- public IRouteTemplateProvider AttributeRoute { get; set; }
-
- public object[] Attributes { get; set; }
-
- public bool RequireActionNameMatch { get; set; }
- }
-}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/ApplicationModelConventions.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/ApplicationModelConventions.cs
new file mode 100644
index 0000000000..7a2af4d017
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/ApplicationModelConventions.cs
@@ -0,0 +1,77 @@
+// 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.
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.AspNet.Mvc.ApplicationModel
+{
+ ///
+ /// Applies conventions to a .
+ ///
+ public static class ApplicationModelConventions
+ {
+ ///
+ /// Applies conventions to a .
+ ///
+ /// The .
+ /// The set of conventions.
+ public static void ApplyConventions(
+ [NotNull] GlobalModel applicationModel,
+ [NotNull] IEnumerable conventions)
+ {
+ // Conventions are applied from the outside-in to allow for scenarios where an action overrides
+ // a controller, etc.
+ foreach (var convention in conventions)
+ {
+ convention.Apply(applicationModel);
+ }
+
+ // First apply the conventions from attributes in decreasing order of scope.
+ foreach (var controller in applicationModel.Controllers)
+ {
+ // ToArray is needed here to prevent issues with modifying the attributes collection
+ // while iterating it.
+ var controllerConventions =
+ controller.Attributes
+ .OfType()
+ .ToArray();
+
+ foreach (var controllerConvention in controllerConventions)
+ {
+ controllerConvention.Apply(controller);
+ }
+
+ foreach (var action in controller.Actions)
+ {
+ // ToArray is needed here to prevent issues with modifying the attributes collection
+ // while iterating it.
+ var actionConventions =
+ action.Attributes
+ .OfType()
+ .ToArray();
+
+ foreach (var actionConvention in actionConventions)
+ {
+ actionConvention.Apply(action);
+ }
+
+ foreach (var parameter in action.Parameters)
+ {
+ // ToArray is needed here to prevent issues with modifying the attributes collection
+ // while iterating it.
+ var parameterConventions =
+ parameter.Attributes
+ .OfType()
+ .ToArray();
+
+ foreach (var parameterConvention in parameterConventions)
+ {
+ parameterConvention.Apply(parameter);
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/DefaultActionModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/DefaultActionModelBuilder.cs
new file mode 100644
index 0000000000..d9309f2fc3
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/DefaultActionModelBuilder.cs
@@ -0,0 +1,252 @@
+// 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.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNet.Mvc.Description;
+using Microsoft.AspNet.Mvc.ModelBinding;
+using Microsoft.AspNet.Mvc.Routing;
+
+namespace Microsoft.AspNet.Mvc.ApplicationModel
+{
+ ///
+ /// A default implementation of .
+ ///
+ public class DefaultActionModelBuilder : IActionModelBuilder
+ {
+ ///
+ public IEnumerable BuildActionModels([NotNull] MethodInfo methodInfo)
+ {
+ if (!IsAction(methodInfo))
+ {
+ return Enumerable.Empty();
+ }
+
+ // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType
+ // is needed to so that the result of ToArray() is object
+ var attributes = methodInfo.GetCustomAttributes(inherit: true).OfType().ToArray();
+
+ // Route attributes create multiple actions, we want to split the set of
+ // attributes based on these so each action only has the attributes that affect it.
+ //
+ // The set of route attributes are split into those that 'define' a route versus those that are
+ // 'silent'.
+ //
+ // We need to define from action for each attribute that 'defines' a route, and a single action
+ // for all of the ones that don't (if any exist).
+ //
+ // Ex:
+ // [HttpGet]
+ // [AcceptVerbs("POST", "PUT")]
+ // [Route("Api/Things")]
+ // public void DoThing()
+ //
+ // This will generate 2 actions:
+ // 1. [Route("Api/Things")]
+ // 2. [HttpGet], [AcceptVerbs("POST", "PUT")]
+ //
+ // Note that having a route attribute that doesn't define a route template _might_ be an error. We
+ // don't have enough context to really know at this point so we just pass it on.
+ var splitAttributes = new List();
+
+ var hasSilentRouteAttribute = false;
+ foreach (var attribute in attributes)
+ {
+ var routeTemplateProvider = attribute as IRouteTemplateProvider;
+ if (routeTemplateProvider != null)
+ {
+ if (IsSilentRouteAttribute(routeTemplateProvider))
+ {
+ hasSilentRouteAttribute = true;
+ }
+ else
+ {
+ splitAttributes.Add(attribute);
+ }
+ }
+ }
+
+ var actionModels = new List();
+ if (splitAttributes.Count == 0 && !hasSilentRouteAttribute)
+ {
+ actionModels.Add(CreateActionModel(methodInfo, attributes));
+ }
+ else
+ {
+ foreach (var splitAttribute in splitAttributes)
+ {
+ var filteredAttributes = new List();
+ foreach (var attribute in attributes)
+ {
+ if (attribute == splitAttribute)
+ {
+ filteredAttributes.Add(attribute);
+ }
+ else if (attribute is IRouteTemplateProvider)
+ {
+ // Exclude other route template providers
+ }
+ else
+ {
+ filteredAttributes.Add(attribute);
+ }
+ }
+
+ actionModels.Add(CreateActionModel(methodInfo, filteredAttributes));
+ }
+
+ if (hasSilentRouteAttribute)
+ {
+ var filteredAttributes = new List();
+ foreach (var attribute in attributes)
+ {
+ if (!splitAttributes.Contains(attribute))
+ {
+ filteredAttributes.Add(attribute);
+ }
+ }
+
+ actionModels.Add(CreateActionModel(methodInfo, filteredAttributes));
+ }
+ }
+
+ foreach (var actionModel in actionModels)
+ {
+ foreach (var parameterInfo in actionModel.ActionMethod.GetParameters())
+ {
+ var parameterModel = CreateParameterModel(parameterInfo);
+ if (parameterModel != null)
+ {
+ parameterModel.Action = actionModel;
+ actionModel.Parameters.Add(parameterModel);
+ }
+ }
+ }
+
+ return actionModels;
+ }
+
+ ///
+ /// Returns true if the is an action. Otherwise false .
+ ///
+ /// The .
+ /// true if the is an action. Otherwise false .
+ ///
+ /// Override this method to provide custom logic to determine which methods are considered actions.
+ ///
+ protected virtual bool IsAction([NotNull] MethodInfo methodInfo)
+ {
+ return
+ methodInfo.IsPublic &&
+ !methodInfo.IsStatic &&
+ !methodInfo.IsAbstract &&
+ !methodInfo.IsConstructor &&
+ !methodInfo.IsGenericMethod &&
+
+ // The SpecialName bit is set to flag members that are treated in a special way by some compilers
+ // (such as property accessors and operator overloading methods).
+ !methodInfo.IsSpecialName &&
+ !methodInfo.IsDefined(typeof(NonActionAttribute)) &&
+
+ // Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid.
+ methodInfo.GetBaseDefinition().DeclaringType != typeof(object);
+ }
+
+ ///
+ /// Creates an for the given .
+ ///
+ /// The .
+ /// The set of attributes to use as metadata.
+ /// An for the given .
+ ///
+ /// An action-method in code may expand into multiple instances depending on how
+ /// the action is routed. In the case of multiple routing attributes, this method will invoked be once for
+ /// each action that can be created.
+ ///
+ /// If overriding this method, use the provided list to find metadata related to
+ /// the action being created.
+ ///
+ protected virtual ActionModel CreateActionModel(
+ [NotNull] MethodInfo methodInfo,
+ [NotNull] IReadOnlyList attributes)
+ {
+ var actionModel = new ActionModel(methodInfo)
+ {
+ IsActionNameMatchRequired = true,
+ };
+
+ actionModel.Attributes.AddRange(attributes);
+
+ actionModel.ActionConstraints.AddRange(attributes.OfType());
+ actionModel.Filters.AddRange(attributes.OfType());
+
+ var actionName = attributes.OfType().FirstOrDefault();
+ if (actionName?.Name != null)
+ {
+ actionModel.ActionName = actionName.Name;
+ }
+ else
+ {
+ actionModel.ActionName = methodInfo.Name;
+ }
+
+ var apiVisibility = attributes.OfType().FirstOrDefault();
+ if (apiVisibility != null)
+ {
+ actionModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
+ }
+
+ var apiGroupName = attributes.OfType().FirstOrDefault();
+ if (apiGroupName != null)
+ {
+ actionModel.ApiExplorerGroupName = apiGroupName.GroupName;
+ }
+
+ var httpMethods = attributes.OfType();
+ actionModel.HttpMethods.AddRange(
+ httpMethods
+ .Where(a => a.HttpMethods != null)
+ .SelectMany(a => a.HttpMethods)
+ .Distinct());
+
+ var routeTemplateProvider = attributes.OfType().FirstOrDefault();
+ if (routeTemplateProvider != null && !IsSilentRouteAttribute(routeTemplateProvider))
+ {
+ actionModel.AttributeRouteModel = new AttributeRouteModel(routeTemplateProvider);
+ }
+
+ return actionModel;
+ }
+
+ ///
+ /// Creates a for the given .
+ ///
+ /// The .
+ /// A for the given .
+ protected virtual ParameterModel CreateParameterModel([NotNull] ParameterInfo parameterInfo)
+ {
+ var parameterModel = new ParameterModel(parameterInfo);
+
+ // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType
+ // is needed to so that the result of ToArray() is object
+ var attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType().ToArray();
+ parameterModel.Attributes.AddRange(attributes);
+
+ parameterModel.BinderMetadata = attributes.OfType().FirstOrDefault();
+
+ parameterModel.ParameterName = parameterInfo.Name;
+ parameterModel.IsOptional = parameterInfo.HasDefaultValue;
+
+ return parameterModel;
+ }
+
+ private bool IsSilentRouteAttribute(IRouteTemplateProvider routeTemplateProvider)
+ {
+ return
+ routeTemplateProvider.Template == null &&
+ routeTemplateProvider.Order == null &&
+ routeTemplateProvider.Name == null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/DefaultControllerModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/DefaultControllerModelBuilder.cs
new file mode 100644
index 0000000000..6bf5fbcd0c
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/DefaultControllerModelBuilder.cs
@@ -0,0 +1,125 @@
+// 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.
+
+using System;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNet.Mvc.Description;
+using Microsoft.AspNet.Mvc.Routing;
+
+namespace Microsoft.AspNet.Mvc.ApplicationModel
+{
+ ///
+ /// A default implementation of .
+ ///
+ public class DefaultControllerModelBuilder : IControllerModelBuilder
+ {
+ private readonly IActionModelBuilder _actionModelBuilder;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The used to create actions.
+ public DefaultControllerModelBuilder(IActionModelBuilder actionModelBuilder)
+ {
+ _actionModelBuilder = actionModelBuilder;
+ }
+
+ ///
+ public ControllerModel BuildControllerModel([NotNull] TypeInfo typeInfo)
+ {
+ if (!IsController(typeInfo))
+ {
+ return null;
+ }
+
+ var controllerModel = CreateControllerModel(typeInfo);
+
+ foreach (var methodInfo in typeInfo.AsType().GetMethods())
+ {
+ var actionModels = _actionModelBuilder.BuildActionModels(methodInfo);
+ if (actionModels != null)
+ {
+ foreach (var actionModel in actionModels)
+ {
+ actionModel.Controller = controllerModel;
+ controllerModel.Actions.Add(actionModel);
+ }
+ }
+ }
+
+ return controllerModel;
+ }
+
+ ///
+ /// Returns true if the is a controller. Otherwise false .
+ ///
+ /// The .
+ /// true if the is a controller. Otherwise false .
+ ///
+ /// Override this method to provide custom logic to determine which types are considered controllers.
+ ///
+ protected virtual bool IsController([NotNull] TypeInfo typeInfo)
+ {
+ if (!typeInfo.IsClass ||
+ typeInfo.IsAbstract ||
+
+ // We only consider public top-level classes as controllers. IsPublic returns false for nested
+ // classes, regardless of visibility modifiers.
+ !typeInfo.IsPublic ||
+ typeInfo.ContainsGenericParameters)
+ {
+ return false;
+ }
+
+ if (typeInfo.Name.Equals("Controller", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ return typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ||
+ typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo);
+ }
+
+ ///
+ /// Creates an for the given .
+ ///
+ /// The .
+ /// A for the given .
+ protected virtual ControllerModel CreateControllerModel([NotNull] TypeInfo typeInfo)
+ {
+ var controllerModel = new ControllerModel(typeInfo);
+
+ // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType
+ // is needed to so that the result of ToArray() is object
+ var attributes = typeInfo.GetCustomAttributes(inherit: true).OfType().ToArray();
+ controllerModel.Attributes.AddRange(attributes);
+
+ controllerModel.ControllerName =
+ typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ?
+ typeInfo.Name.Substring(0, typeInfo.Name.Length - "Controller".Length) :
+ typeInfo.Name;
+
+ controllerModel.ActionConstraints.AddRange(attributes.OfType());
+ controllerModel.Filters.AddRange(attributes.OfType());
+ controllerModel.RouteConstraints.AddRange(attributes.OfType());
+
+ controllerModel.AttributeRoutes.AddRange(
+ attributes.OfType().Select(rtp => new AttributeRouteModel(rtp)));
+
+ var apiVisibility = attributes.OfType().FirstOrDefault();
+ if (apiVisibility != null)
+ {
+ controllerModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
+ }
+
+ var apiGroupName = attributes.OfType().FirstOrDefault();
+ if (apiGroupName != null)
+ {
+ controllerModel.ApiExplorerGroupName = apiGroupName.GroupName;
+ }
+
+ return controllerModel;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/IActionModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/IActionModelBuilder.cs
new file mode 100644
index 0000000000..fe6eb58e4c
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/IActionModelBuilder.cs
@@ -0,0 +1,26 @@
+// 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.
+
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Microsoft.AspNet.Mvc.ApplicationModel
+{
+ ///
+ /// Creates a set of for a method.
+ ///
+ public interface IActionModelBuilder
+ {
+ ///
+ /// Creates a set of for a method. May return null or empty if
+ /// is not an action method.
+ ///
+ /// The .
+ /// A set of or null.
+ ///
+ /// Instances of returned from this interface should have their
+ /// initialized.
+ ///
+ IEnumerable BuildActionModels([NotNull] MethodInfo methodInfo);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/IControllerModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/IControllerModelBuilder.cs
new file mode 100644
index 0000000000..49b85d895b
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModel/IControllerModelBuilder.cs
@@ -0,0 +1,25 @@
+// 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.
+
+using System.Reflection;
+
+namespace Microsoft.AspNet.Mvc.ApplicationModel
+{
+ ///
+ /// Creates a set of for a type.
+ ///
+ public interface IControllerModelBuilder
+ {
+ ///
+ /// Creates a set of for a type. May return null or empty if
+ /// is not a controller type.
+ ///
+ /// The .
+ /// A or null.
+ ///
+ /// Instances of returned from this interface should have their
+ /// initialized.
+ ///
+ ControllerModel BuildControllerModel([NotNull] TypeInfo typeInfo);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs
new file mode 100644
index 0000000000..0cf1088a8f
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs
@@ -0,0 +1,830 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNet.Mvc.ApplicationModel;
+using Microsoft.AspNet.Mvc.Core;
+using Microsoft.AspNet.Mvc.Description;
+using Microsoft.AspNet.Mvc.Routing;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Creates instances of from .
+ ///
+ public static class ControllerActionDescriptorBuilder
+ {
+ // This is the default order for attribute routes whose order calculated from
+ // the controller model is null.
+ private const int DefaultAttributeRouteOrder = 0;
+
+ ///
+ /// Creates instances of from .
+ ///
+ /// The .
+ /// The list of .
+ public static IList Build(GlobalModel application)
+ {
+ var actions = new List();
+
+ 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 application.Controllers)
+ {
+ var controllerDescriptor = new ControllerDescriptor()
+ {
+ ControllerTypeInfo = controller.ControllerType,
+ Name = controller.ControllerName,
+ };
+
+ foreach (var action in controller.Actions)
+ {
+ // 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(application, controller, action);
+
+ foreach (var actionDescriptor in actionDescriptors)
+ {
+ actionDescriptor.ControllerDescriptor = controllerDescriptor;
+
+ AddApiExplorerInfo(actionDescriptor, action, controller);
+ AddRouteConstraints(actionDescriptor, controller, action);
+ AddControllerRouteConstraints(
+ actionDescriptor,
+ controller.RouteConstraints,
+ removalConstraints);
+
+ if (IsAttributeRoutedAction(actionDescriptor))
+ {
+ hasAttributeRoutes = true;
+
+ // An attribute routed action will ignore conventional routed constraints. We still
+ // want to provide these values as ambient values for link generation.
+ AddConstraintsAsDefaultRouteValues(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);
+ }
+ }
+
+ 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
+ // selected when a route group returned by the route.
+ if (hasAttributeRoutes)
+ {
+ actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
+ AttributeRouting.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 incomming request.
+ AddRemovalConstraints(actionDescriptor, removalConstraints);
+ }
+ else
+ {
+ var attributeRouteInfo = actionDescriptor.AttributeRouteInfo;
+ if (attributeRouteInfo.Name != null)
+ {
+ // Build a map of attribute route name to action descriptors to ensure that all
+ // attribute routes with a given name have the same template.
+ AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
+ }
+
+ // We still want to add a 'null' for any constraint with DenyKey so that link generation
+ // works properly.
+ //
+ // 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)
+ {
+ if (!actionDescriptor.RouteValueDefaults.ContainsKey(key))
+ {
+ actionDescriptor.RouteValueDefaults.Add(key, value: null);
+ }
+ }
+ }
+ }
+
+ if (attributeRoutingConfigurationErrors.Any())
+ {
+ var message = CreateAttributeRoutingAggregateErrorMessage(
+ attributeRoutingConfigurationErrors.Values);
+
+ throw new InvalidOperationException(message);
+ }
+
+ var namedRoutedErrors = ValidateNamedAttributeRoutedActions(actionsByRouteName);
+ if (namedRoutedErrors.Any())
+ {
+ var message = CreateAttributeRoutingAggregateErrorMessage(namedRoutedErrors);
+ throw new InvalidOperationException(message);
+ }
+
+ if (routeTemplateErrors.Any())
+ {
+ var message = CreateAttributeRoutingAggregateErrorMessage(routeTemplateErrors);
+ throw new InvalidOperationException(message);
+ }
+
+ return actions;
+ }
+
+ private static IList CreateActionDescriptors(
+ GlobalModel application,
+ ControllerModel controller,
+ ActionModel action)
+ {
+ 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
+ if (action.AttributeRouteModel != null &&
+ action.AttributeRouteModel.IsAbsoluteTemplate)
+ {
+ // We're overriding the attribute routes on the controller, so filter out any metadata
+ // from controller level routes.
+ var actionDescriptor = CreateActionDescriptor(
+ action,
+ controllerAttributeRoute: null);
+
+ actionDescriptors.Add(actionDescriptor);
+
+ // If we're using an attribute route on the controller, then filter out any additional
+ // metadata from the 'other' attribute routes.
+ var controllerFilters = controller.Filters
+ .Where(c => !(c is IRouteTemplateProvider));
+ AddActionFilters(actionDescriptor, action.Filters, controllerFilters, application.Filters);
+
+ var controllerConstraints = controller.ActionConstraints
+ .Where(c => !(c is IRouteTemplateProvider));
+ AddActionConstraints(actionDescriptor, action, controllerConstraints);
+ }
+ else if (controller.AttributeRoutes != null &&
+ controller.AttributeRoutes.Count > 0)
+ {
+ // We're using the attribute routes from the controller
+ foreach (var controllerAttributeRoute in controller.AttributeRoutes)
+ {
+ var actionDescriptor = CreateActionDescriptor(
+ action,
+ controllerAttributeRoute);
+
+ actionDescriptors.Add(actionDescriptor);
+
+ // If we're using an attribute route on the controller, then filter out any additional
+ // metadata from the 'other' attribute routes.
+ var controllerFilters = controller.Filters
+ .Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider));
+ AddActionFilters(actionDescriptor, action.Filters, controllerFilters, application.Filters);
+
+ var controllerConstraints = controller.ActionConstraints
+ .Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider));
+ AddActionConstraints(actionDescriptor, action, controllerConstraints);
+ }
+ }
+ else
+ {
+ // No attribute routes on the controller
+ var actionDescriptor = CreateActionDescriptor(
+ action,
+ controllerAttributeRoute: null);
+ actionDescriptors.Add(actionDescriptor);
+
+ // If there's no attribute route on the controller, then we can use all of the filters/constraints
+ // on the controller.
+ AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters);
+ AddActionConstraints(actionDescriptor, action, controller.ActionConstraints);
+ }
+
+ return actionDescriptors;
+ }
+
+ private static ControllerActionDescriptor CreateActionDescriptor(
+ ActionModel action,
+ AttributeRouteModel controllerAttributeRoute)
+ {
+ var parameterDescriptors = new List();
+ foreach (var parameter in action.Parameters)
+ {
+ var parameterDescriptor = CreateParameterDescriptor(parameter);
+ parameterDescriptors.Add(parameterDescriptor);
+ }
+
+ var attributeRouteInfo = CreateAttributeRouteInfo(
+ action.AttributeRouteModel,
+ controllerAttributeRoute);
+
+ var actionDescriptor = new ControllerActionDescriptor()
+ {
+ Name = action.ActionName,
+ MethodInfo = action.ActionMethod,
+ Parameters = parameterDescriptors,
+ RouteConstraints = new List(),
+ AttributeRouteInfo = attributeRouteInfo,
+ };
+
+ actionDescriptor.DisplayName = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}.{1}",
+ action.ActionMethod.DeclaringType.FullName,
+ action.ActionMethod.Name);
+
+ return actionDescriptor;
+ }
+
+ private static ParameterDescriptor CreateParameterDescriptor(ParameterModel parameter)
+ {
+ var parameterDescriptor = new ParameterDescriptor()
+ {
+ BinderMetadata = parameter.BinderMetadata,
+ IsOptional = parameter.IsOptional,
+ Name = parameter.ParameterName,
+ ParameterType = parameter.ParameterInfo.ParameterType,
+ };
+
+ return parameterDescriptor;
+ }
+
+ private static void AddApiExplorerInfo(
+ ControllerActionDescriptor actionDescriptor,
+ ActionModel action,
+ ControllerModel controller)
+ {
+ var apiExplorerIsVisible = action.ApiExplorerIsVisible ?? controller.ApiExplorerIsVisible ?? false;
+ if (apiExplorerIsVisible)
+ {
+ var apiExplorerActionData = new ApiDescriptionActionData()
+ {
+ GroupName = action.ApiExplorerGroupName ?? controller.ApiExplorerGroupName,
+ };
+
+ actionDescriptor.SetProperty(apiExplorerActionData);
+ }
+ }
+
+ private static void AddActionFilters(
+ ControllerActionDescriptor 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(
+ AttributeRouteModel action,
+ AttributeRouteModel controller)
+ {
+ var combinedRoute = AttributeRouteModel.CombineAttributeRouteModel(
+ controller,
+ action);
+
+ if (combinedRoute == null)
+ {
+ return null;
+ }
+ else
+ {
+ return new AttributeRouteInfo()
+ {
+ Template = combinedRoute.Template,
+ Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
+ Name = combinedRoute.Name,
+ };
+ }
+ }
+
+ private static void AddActionConstraints(
+ ControllerActionDescriptor actionDescriptor,
+ ActionModel action,
+ IEnumerable controllerConstraints)
+ {
+ var constraints = new List();
+
+ var httpMethods = action.HttpMethods;
+ if (httpMethods != null && httpMethods.Count > 0)
+ {
+ constraints.Add(new HttpMethodConstraint(httpMethods));
+ }
+
+ if (action.ActionConstraints != null)
+ {
+ constraints.AddRange(action.ActionConstraints);
+ }
+
+ if (controllerConstraints != null)
+ {
+ constraints.AddRange(controllerConstraints);
+ }
+
+ if (constraints.Count > 0)
+ {
+ actionDescriptor.ActionConstraints = constraints;
+ }
+ }
+
+ public static void AddRouteConstraints(
+ 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 routeconstraints,
+ ISet 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
+ // 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)
+ {
+ if (constraintAttribute.BlockNonAttributedActions)
+ {
+ removalConstraints.Add(constraintAttribute.RouteKey);
+ }
+
+ // Skip duplicates
+ 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));
+ }
+ }
+ }
+ }
+
+ private static bool HasConstraint(List constraints, string routeKey)
+ {
+ return constraints.Any(
+ rc => string.Equals(rc.RouteKey, routeKey, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static void ReplaceRouteConstraints(ControllerActionDescriptor actionDescriptor)
+ {
+ var routeGroupValue = GetRouteGroupValue(
+ actionDescriptor.AttributeRouteInfo.Order,
+ actionDescriptor.AttributeRouteInfo.Template);
+
+ var routeConstraints = new List();
+ routeConstraints.Add(new RouteDataActionConstraint(
+ AttributeRouting.RouteGroupKey,
+ routeGroupValue));
+
+ actionDescriptor.RouteConstraints = routeConstraints;
+ }
+
+ private static void ReplaceAttributeRouteTokens(
+ ControllerActionDescriptor actionDescriptor,
+ IList routeTemplateErrors)
+ {
+ try
+ {
+ actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens(
+ actionDescriptor.AttributeRouteInfo.Template,
+ actionDescriptor.RouteValueDefaults);
+ }
+ catch (InvalidOperationException ex)
+ {
+ // Routing will throw an InvalidOperationException here if we can't parse/replace tokens
+ // in the template.
+ var message = Resources.FormatAttributeRoute_IndividualErrorMessage(
+ actionDescriptor.DisplayName,
+ Environment.NewLine,
+ ex.Message);
+
+ routeTemplateErrors.Add(message);
+ }
+ }
+
+ private static void AddConstraintsAsDefaultRouteValues(ControllerActionDescriptor actionDescriptor)
+ {
+ foreach (var constraint in actionDescriptor.RouteConstraints)
+ {
+ // We don't need to do anything with attribute routing for 'catch all' behavior. Order
+ // and predecedence of attribute routes allow this kind of behavior.
+ if (constraint.KeyHandling == RouteKeyHandling.RequireKey ||
+ constraint.KeyHandling == RouteKeyHandling.DenyKey)
+ {
+ actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
+ }
+ }
+ }
+
+ private static void AddRemovalConstraints(
+ ControllerActionDescriptor actionDescriptor,
+ ISet removalConstraints)
+ {
+ foreach (var key in removalConstraints)
+ {
+ if (!HasConstraint(actionDescriptor.RouteConstraints, key))
+ {
+ actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
+ key,
+ string.Empty));
+ }
+ }
+ }
+
+ private static void AddActionToNamedGroup(
+ IDictionary> actionsByRouteName,
+ string routeName,
+ ControllerActionDescriptor actionDescriptor)
+ {
+ IList namedActionGroup;
+
+ if (actionsByRouteName.TryGetValue(routeName, out namedActionGroup))
+ {
+ namedActionGroup.Add(actionDescriptor);
+ }
+ else
+ {
+ namedActionGroup = new List();
+ namedActionGroup.Add(actionDescriptor);
+ actionsByRouteName.Add(routeName, namedActionGroup);
+ }
+ }
+
+ private static bool IsAttributeRoutedAction(ControllerActionDescriptor actionDescriptor)
+ {
+ return actionDescriptor.AttributeRouteInfo?.Template != null;
+ }
+
+ private static IList AddErrorNumbers(
+ IEnumerable namedRoutedErrors)
+ {
+ return namedRoutedErrors
+ .Select((error, i) =>
+ Resources.FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(
+ i + 1,
+ Environment.NewLine,
+ error))
+ .ToList();
+ }
+
+ private static IList ValidateNamedAttributeRoutedActions(
+ IDictionary> actionsGroupedByRouteName)
+ {
+ var namedRouteErrors = new List();
+
+ foreach (var kvp in actionsGroupedByRouteName)
+ {
+ // We are looking for attribute routed actions that have the same name but
+ // different route templates. We pick the first template of the group and
+ // we compare it against the rest of the templates that have that same name
+ // associated.
+ // The moment we find one that is different we report the whole group to the
+ // user in the error message so that he can see the different actions and the
+ // different templates for a given named attribute route.
+ var firstActionDescriptor = kvp.Value[0];
+ var firstTemplate = firstActionDescriptor.AttributeRouteInfo.Template;
+
+ for (var i = 1; i < kvp.Value.Count; i++)
+ {
+ var otherActionDescriptor = kvp.Value[i];
+ var otherActionTemplate = otherActionDescriptor.AttributeRouteInfo.Template;
+
+ if (!firstTemplate.Equals(otherActionTemplate, StringComparison.OrdinalIgnoreCase))
+ {
+ var descriptions = kvp.Value.Select(ad =>
+ Resources.FormatAttributeRoute_DuplicateNames_Item(
+ ad.DisplayName,
+ ad.AttributeRouteInfo.Template));
+
+ var errorDescription = string.Join(Environment.NewLine, descriptions);
+ var message = Resources.FormatAttributeRoute_DuplicateNames(
+ kvp.Key,
+ Environment.NewLine,
+ errorDescription);
+
+ namedRouteErrors.Add(message);
+ break;
+ }
+ }
+ }
+
+ return namedRouteErrors;
+ }
+
+ private static void ValidateActionGroupConfiguration(
+ IDictionary>> methodMap,
+ ControllerActionDescriptor 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(
+ ActionModel 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(
+ ControllerActionDescriptor 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(
+ CultureInfo.InvariantCulture,
+ "{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(
+ ControllerActionDescriptor 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(
+ CultureInfo.InvariantCulture,
+ "{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(CultureInfo.InvariantCulture, "{0}-{1}", order, template);
+ return ("__route__" + group).ToUpperInvariant();
+ }
+
+ // We need to build a map of methods to reflected actions and reflected actions to
+ // 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(ActionModel 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);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs
index e29fd4f882..528f6f4bf2 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs
@@ -5,34 +5,26 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
-using Microsoft.AspNet.Mvc.ApplicationModel;
-using Microsoft.AspNet.Mvc.Core;
-using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Filters;
-using Microsoft.AspNet.Mvc.ModelBinding;
-using Microsoft.AspNet.Mvc.Routing;
+using Microsoft.AspNet.Mvc.ApplicationModel;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
{
public class ControllerActionDescriptorProvider : IActionDescriptorProvider
{
- // This is the default order for attribute routes whose order calculated from
- // the reflected model is null.
- private const int DefaultAttributeRouteOrder = 0;
-
+ private readonly IControllerModelBuilder _applicationModelBuilder;
private readonly IAssemblyProvider _assemblyProvider;
- private readonly IActionDiscoveryConventions _conventions;
private readonly IReadOnlyList _globalFilters;
private readonly IEnumerable _modelConventions;
public ControllerActionDescriptorProvider(IAssemblyProvider assemblyProvider,
- IActionDiscoveryConventions conventions,
+ IControllerModelBuilder applicationModelBuilder,
IGlobalFilterProvider globalFilters,
IOptions optionsAccessor)
{
_assemblyProvider = assemblyProvider;
- _conventions = conventions;
+ _applicationModelBuilder = applicationModelBuilder;
_globalFilters = globalFilters.Filters;
_modelConventions = optionsAccessor.Options.ApplicationModelConventions;
}
@@ -50,9 +42,9 @@ namespace Microsoft.AspNet.Mvc
public IEnumerable GetDescriptors()
{
- var model = BuildModel();
- ApplyConventions(model);
- return Build(model);
+ var applicationModel = BuildModel();
+ ApplicationModelConventions.ApplyConventions(applicationModel, _modelConventions);
+ return ControllerActionDescriptorBuilder.Build(applicationModel);
}
public GlobalModel BuildModel()
@@ -62,991 +54,18 @@ namespace Microsoft.AspNet.Mvc
var assemblies = _assemblyProvider.CandidateAssemblies;
var types = assemblies.SelectMany(a => a.DefinedTypes);
- var controllerTypes = types.Where(_conventions.IsController);
- foreach (var controllerType in controllerTypes)
+ foreach (var type in types)
{
- var controllerModel = CreateControllerModel(applicationModel, controllerType);
- applicationModel.Controllers.Add(controllerModel);
-
- foreach (var methodInfo in controllerType.AsType().GetMethods())
+ var controllerModel = _applicationModelBuilder.BuildControllerModel(type);
+ if (controllerModel != null)
{
- var actionInfos = _conventions.GetActions(methodInfo, controllerType);
- if (actionInfos == null)
- {
- continue;
- }
-
- foreach (var actionInfo in actionInfos)
- {
- var actionModel = CreateActionModel(controllerModel, methodInfo, actionInfo);
- controllerModel.Actions.Add(actionModel);
-
- foreach (var parameterInfo in methodInfo.GetParameters())
- {
- var parameterModel = CreateParameterModel(actionModel, parameterInfo);
- actionModel.Parameters.Add(parameterModel);
- }
- }
+ controllerModel.Application = applicationModel;
+ applicationModel.Controllers.Add(controllerModel);
}
}
return applicationModel;
}
-
- private ControllerModel CreateControllerModel(
- GlobalModel applicationModel,
- TypeInfo controllerType)
- {
- var controllerModel = new ControllerModel(controllerType)
- {
- Application = applicationModel,
- };
-
- controllerModel.ControllerName =
- controllerType.Name.EndsWith("Controller", StringComparison.Ordinal) ?
- controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length) :
- controllerType.Name;
-
- // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType
- // is needed to so that the result of ToList() is List
- var attributes = controllerType.GetCustomAttributes(inherit: true).ToList();
- controllerModel.Attributes.AddRange(attributes);
-
- controllerModel.ActionConstraints.AddRange(attributes.OfType());
- controllerModel.Filters.AddRange(attributes.OfType());
- controllerModel.RouteConstraints.AddRange(attributes.OfType());
-
- controllerModel.AttributeRoutes.AddRange(
- attributes.OfType().Select(rtp => new AttributeRouteModel(rtp)));
-
- var apiVisibility = attributes.OfType().FirstOrDefault();
- if (apiVisibility != null)
- {
- controllerModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
- }
-
- var apiGroupName = attributes.OfType().FirstOrDefault();
- if (apiGroupName != null)
- {
- controllerModel.ApiExplorerGroupName = apiGroupName.GroupName;
- }
-
- return controllerModel;
- }
-
- private ActionModel CreateActionModel(
- ControllerModel controllerModel,
- MethodInfo methodInfo,
- ActionInfo actionInfo)
- {
- var actionModel = new ActionModel(methodInfo)
- {
- ActionName = actionInfo.ActionName,
- Controller = controllerModel,
- IsActionNameMatchRequired = actionInfo.RequireActionNameMatch,
- };
-
- var attributes = actionInfo.Attributes;
-
- actionModel.Attributes.AddRange(attributes);
-
- actionModel.ActionConstraints.AddRange(attributes.OfType());
- actionModel.Filters.AddRange(attributes.OfType());
-
- var apiVisibility = attributes.OfType().FirstOrDefault();
- if (apiVisibility != null)
- {
- actionModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
- }
-
- var apiGroupName = attributes.OfType().FirstOrDefault();
- if (apiGroupName != null)
- {
- actionModel.ApiExplorerGroupName = apiGroupName.GroupName;
- }
-
- actionModel.HttpMethods.AddRange(actionInfo.HttpMethods ?? Enumerable.Empty());
-
- if (actionInfo.AttributeRoute != null)
- {
- actionModel.AttributeRouteModel = new AttributeRouteModel(
- actionInfo.AttributeRoute);
- }
-
- return actionModel;
- }
-
- private ParameterModel CreateParameterModel(
- ActionModel actionModel,
- ParameterInfo parameterInfo)
- {
- var parameterModel = new ParameterModel(parameterInfo)
- {
- Action = actionModel,
- };
-
- // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType
- // is needed to so that the result of ToList() is List
- var attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType().ToList();
- parameterModel.Attributes.AddRange(attributes);
-
- parameterModel.BinderMetadata = attributes.OfType().FirstOrDefault();
-
- parameterModel.ParameterName = parameterInfo.Name;
- parameterModel.IsOptional = parameterInfo.HasDefaultValue;
-
- return parameterModel;
- }
-
- public void ApplyConventions(GlobalModel model)
- {
- // Conventions are applied from the outside-in to allow for scenarios where an action overrides
- // a controller, etc.
- foreach (var convention in _modelConventions)
- {
- convention.Apply(model);
- }
-
- // First apply the conventions from attributes in decreasing order of scope.
- foreach (var controller in model.Controllers)
- {
- // ToArray is needed here to prevent issues with modifying the attributes collection
- // while iterating it.
- var controllerConventions =
- controller.Attributes
- .OfType()
- .ToArray();
-
- foreach (var controllerConvention in controllerConventions)
- {
- controllerConvention.Apply(controller);
- }
-
- foreach (var action in controller.Actions)
- {
- // ToArray is needed here to prevent issues with modifying the attributes collection
- // while iterating it.
- var actionConventions =
- action.Attributes
- .OfType()
- .ToArray();
-
- foreach (var actionConvention in actionConventions)
- {
- actionConvention.Apply(action);
- }
-
- foreach (var parameter in action.Parameters)
- {
- // ToArray is needed here to prevent issues with modifying the attributes collection
- // while iterating it.
- var parameterConventions =
- parameter.Attributes
- .OfType()
- .ToArray();
-
- foreach (var parameterConvention in parameterConventions)
- {
- parameterConvention.Apply(parameter);
- }
- }
- }
- }
- }
-
- public List Build(GlobalModel application)
- {
- var actions = new List();
-
- 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 application.Controllers)
- {
- var controllerDescriptor = new ControllerDescriptor()
- {
- ControllerTypeInfo = controller.ControllerType,
- Name = controller.ControllerName,
- };
-
- foreach (var action in controller.Actions)
- {
- // 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(application, controller, action);
-
- foreach (var actionDescriptor in actionDescriptors)
- {
- actionDescriptor.ControllerDescriptor = controllerDescriptor;
-
- AddApiExplorerInfo(actionDescriptor, action, controller);
- AddRouteConstraints(actionDescriptor, controller, action);
- AddControllerRouteConstraints(
- actionDescriptor,
- controller.RouteConstraints,
- removalConstraints);
-
- if (IsAttributeRoutedAction(actionDescriptor))
- {
- hasAttributeRoutes = true;
-
- // An attribute routed action will ignore conventional routed constraints. We still
- // want to provide these values as ambient values for link generation.
- AddConstraintsAsDefaultRouteValues(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);
- }
- }
-
- 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
- // selected when a route group returned by the route.
- if (hasAttributeRoutes)
- {
- actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
- AttributeRouting.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 incomming request.
- AddRemovalConstraints(actionDescriptor, removalConstraints);
- }
- else
- {
- var attributeRouteInfo = actionDescriptor.AttributeRouteInfo;
- if (attributeRouteInfo.Name != null)
- {
- // Build a map of attribute route name to action descriptors to ensure that all
- // attribute routes with a given name have the same template.
- AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
- }
-
- // We still want to add a 'null' for any constraint with DenyKey so that link generation
- // works properly.
- //
- // 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)
- {
- if (!actionDescriptor.RouteValueDefaults.ContainsKey(key))
- {
- actionDescriptor.RouteValueDefaults.Add(key, null);
- }
- }
- }
- }
-
- if (attributeRoutingConfigurationErrors.Any())
- {
- var message = CreateAttributeRoutingAggregateErrorMessage(
- attributeRoutingConfigurationErrors.Values);
-
- throw new InvalidOperationException(message);
- }
-
- var namedRoutedErrors = ValidateNamedAttributeRoutedActions(actionsByRouteName);
- if (namedRoutedErrors.Any())
- {
- var message = CreateAttributeRoutingAggregateErrorMessage(namedRoutedErrors);
- throw new InvalidOperationException(message);
- }
-
- if (routeTemplateErrors.Any())
- {
- var message = CreateAttributeRoutingAggregateErrorMessage(routeTemplateErrors);
- throw new InvalidOperationException(message);
- }
-
- return actions;
- }
-
- private static IList CreateActionDescriptors(
- GlobalModel application,
- ControllerModel controller,
- ActionModel action)
- {
- 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
- if (action.AttributeRouteModel != null &&
- action.AttributeRouteModel.IsAbsoluteTemplate)
- {
- // We're overriding the attribute routes on the controller, so filter out any metadata
- // from controller level routes.
- var actionDescriptor = CreateActionDescriptor(
- action,
- controllerAttributeRoute: null);
-
- actionDescriptors.Add(actionDescriptor);
-
- // If we're using an attribute route on the controller, then filter out any additional
- // metadata from the 'other' attribute routes.
- var controllerFilters = controller.Filters
- .Where(c => !(c is IRouteTemplateProvider));
- AddActionFilters(actionDescriptor, action.Filters, controllerFilters, application.Filters);
-
- var controllerConstraints = controller.ActionConstraints
- .Where(c => !(c is IRouteTemplateProvider));
- AddActionConstraints(actionDescriptor, action, controllerConstraints);
- }
- else if (controller.AttributeRoutes != null &&
- controller.AttributeRoutes.Count > 0)
- {
- // We're using the attribute routes from the controller
- foreach (var controllerAttributeRoute in controller.AttributeRoutes)
- {
- var actionDescriptor = CreateActionDescriptor(
- action,
- controllerAttributeRoute);
-
- actionDescriptors.Add(actionDescriptor);
-
- // If we're using an attribute route on the controller, then filter out any additional
- // metadata from the 'other' attribute routes.
- var controllerFilters = controller.Filters
- .Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider));
- AddActionFilters(actionDescriptor, action.Filters, controllerFilters, application.Filters);
-
- var controllerConstraints = controller.ActionConstraints
- .Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider));
- AddActionConstraints(actionDescriptor, action, controllerConstraints);
- }
- }
- else
- {
- // No attribute routes on the controller
- var actionDescriptor = CreateActionDescriptor(
- action,
- controllerAttributeRoute: null);
- actionDescriptors.Add(actionDescriptor);
-
- // If there's no attribute route on the controller, then we can use all of the filters/constraints
- // on the controller.
- AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters);
- AddActionConstraints(actionDescriptor, action, controller.ActionConstraints);
- }
-
- return actionDescriptors;
- }
-
- private static ControllerActionDescriptor CreateActionDescriptor(
- ActionModel action,
- AttributeRouteModel controllerAttributeRoute)
- {
- var parameterDescriptors = new List();
- foreach (var parameter in action.Parameters)
- {
- var parameterDescriptor = CreateParameterDescriptor(parameter);
- parameterDescriptors.Add(parameterDescriptor);
- }
-
- var attributeRouteInfo = CreateAttributeRouteInfo(
- action.AttributeRouteModel,
- controllerAttributeRoute);
-
- var actionDescriptor = new ControllerActionDescriptor()
- {
- Name = action.ActionName,
- MethodInfo = action.ActionMethod,
- Parameters = parameterDescriptors,
- RouteConstraints = new List(),
- AttributeRouteInfo = attributeRouteInfo
- };
-
- actionDescriptor.DisplayName = string.Format(
- "{0}.{1}",
- action.ActionMethod.DeclaringType.FullName,
- action.ActionMethod.Name);
-
- return actionDescriptor;
- }
-
- private static ParameterDescriptor CreateParameterDescriptor(ParameterModel parameter)
- {
- var parameterDescriptor = new ParameterDescriptor()
- {
- BinderMetadata = parameter.BinderMetadata,
- IsOptional = parameter.IsOptional,
- Name = parameter.ParameterName,
- ParameterType = parameter.ParameterInfo.ParameterType,
- };
-
- return parameterDescriptor;
- }
-
- private static void AddApiExplorerInfo(
- ControllerActionDescriptor actionDescriptor,
- ActionModel action,
- ControllerModel controller)
- {
- var apiExplorerIsVisible = action.ApiExplorerIsVisible ?? controller.ApiExplorerIsVisible ?? false;
- if (apiExplorerIsVisible)
- {
- var apiExplorerActionData = new ApiDescriptionActionData()
- {
- GroupName = action.ApiExplorerGroupName ?? controller.ApiExplorerGroupName,
- };
-
- actionDescriptor.SetProperty(apiExplorerActionData);
- }
- }
-
- private static void AddActionFilters(
- ControllerActionDescriptor 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(
- AttributeRouteModel action,
- AttributeRouteModel controller)
- {
- var combinedRoute = AttributeRouteModel.CombineAttributeRouteModel(
- controller,
- action);
-
- if (combinedRoute == null)
- {
- return null;
- }
- else
- {
- return new AttributeRouteInfo()
- {
- Template = combinedRoute.Template,
- Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
- Name = combinedRoute.Name,
- };
- }
- }
-
- private static void AddActionConstraints(
- ControllerActionDescriptor actionDescriptor,
- ActionModel action,
- IEnumerable controllerConstraints)
- {
- var constraints = new List();
-
- var httpMethods = action.HttpMethods;
- if (httpMethods != null && httpMethods.Count > 0)
- {
- constraints.Add(new HttpMethodConstraint(httpMethods));
- }
-
- if (action.ActionConstraints != null)
- {
- constraints.AddRange(action.ActionConstraints);
- }
-
- if (controllerConstraints != null)
- {
- constraints.AddRange(controllerConstraints);
- }
-
- if (constraints.Count > 0)
- {
- actionDescriptor.ActionConstraints = constraints;
- }
- }
-
- public void AddRouteConstraints(
- 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 routeconstraints,
- ISet 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
- // 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)
- {
- if (constraintAttribute.BlockNonAttributedActions)
- {
- removalConstraints.Add(constraintAttribute.RouteKey);
- }
-
- // Skip duplicates
- 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));
- }
- }
- }
- }
-
- private static bool HasConstraint(List constraints, string routeKey)
- {
- return constraints.Any(
- rc => string.Equals(rc.RouteKey, routeKey, StringComparison.OrdinalIgnoreCase));
- }
-
- private static void ReplaceRouteConstraints(ControllerActionDescriptor actionDescriptor)
- {
- var routeGroupValue = GetRouteGroupValue(
- actionDescriptor.AttributeRouteInfo.Order,
- actionDescriptor.AttributeRouteInfo.Template);
-
- var routeConstraints = new List();
- routeConstraints.Add(new RouteDataActionConstraint(
- AttributeRouting.RouteGroupKey,
- routeGroupValue));
-
- actionDescriptor.RouteConstraints = routeConstraints;
- }
-
- private static void ReplaceAttributeRouteTokens(
- ControllerActionDescriptor actionDescriptor,
- IList routeTemplateErrors)
- {
- try
- {
- actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens(
- actionDescriptor.AttributeRouteInfo.Template,
- actionDescriptor.RouteValueDefaults);
- }
- catch (InvalidOperationException ex)
- {
- var message = Resources.FormatAttributeRoute_IndividualErrorMessage(
- actionDescriptor.DisplayName,
- Environment.NewLine,
- ex.Message);
-
- routeTemplateErrors.Add(message);
- }
- }
-
- private static void AddConstraintsAsDefaultRouteValues(ControllerActionDescriptor actionDescriptor)
- {
- foreach (var constraint in actionDescriptor.RouteConstraints)
- {
- // We don't need to do anything with attribute routing for 'catch all' behavior. Order
- // and predecedence of attribute routes allow this kind of behavior.
- if (constraint.KeyHandling == RouteKeyHandling.RequireKey ||
- constraint.KeyHandling == RouteKeyHandling.DenyKey)
- {
- actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
- }
- }
- }
-
- private static void AddRemovalConstraints(
- ControllerActionDescriptor actionDescriptor,
- ISet removalConstraints)
- {
- foreach (var key in removalConstraints)
- {
- if (!HasConstraint(actionDescriptor.RouteConstraints, key))
- {
- actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
- key,
- string.Empty));
- }
- }
- }
-
- private static void AddActionToNamedGroup(
- IDictionary> actionsByRouteName,
- string routeName,
- ControllerActionDescriptor actionDescriptor)
- {
- IList namedActionGroup;
-
- if (actionsByRouteName.TryGetValue(routeName, out namedActionGroup))
- {
- namedActionGroup.Add(actionDescriptor);
- }
- else
- {
- namedActionGroup = new List();
- namedActionGroup.Add(actionDescriptor);
- actionsByRouteName.Add(routeName, namedActionGroup);
- }
- }
-
- private static bool IsAttributeRoutedAction(ControllerActionDescriptor actionDescriptor)
- {
- return actionDescriptor.AttributeRouteInfo != null &&
- actionDescriptor.AttributeRouteInfo.Template != null;
- }
-
- private static IList AddErrorNumbers(
- IEnumerable namedRoutedErrors)
- {
- return namedRoutedErrors
- .Select((nre, i) =>
- Resources.FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(
- i + 1,
- Environment.NewLine,
- nre))
- .ToList();
- }
-
- private static IList ValidateNamedAttributeRoutedActions(
- IDictionary> actionsGroupedByRouteName)
- {
- var namedRouteErrors = new List();
-
- foreach (var kvp in actionsGroupedByRouteName)
- {
- // We are looking for attribute routed actions that have the same name but
- // different route templates. We pick the first template of the group and
- // we compare it against the rest of the templates that have that same name
- // associated.
- // The moment we find one that is different we report the whole group to the
- // user in the error message so that he can see the different actions and the
- // different templates for a given named attribute route.
- var firstActionDescriptor = kvp.Value[0];
- var firstTemplate = firstActionDescriptor.AttributeRouteInfo.Template;
-
- for (var i = 1; i < kvp.Value.Count; i++)
- {
- var otherActionDescriptor = kvp.Value[i];
- var otherActionTemplate = otherActionDescriptor.AttributeRouteInfo.Template;
-
- if (!firstTemplate.Equals(otherActionTemplate, StringComparison.OrdinalIgnoreCase))
- {
- var descriptions = kvp.Value.Select(ad =>
- Resources.FormatAttributeRoute_DuplicateNames_Item(
- ad.DisplayName,
- ad.AttributeRouteInfo.Template));
-
- var errorDescription = string.Join(Environment.NewLine, descriptions);
- var message = Resources.FormatAttributeRoute_DuplicateNames(
- kvp.Key,
- Environment.NewLine,
- errorDescription);
-
- namedRouteErrors.Add(message);
- break;
- }
- }
- }
-
- return namedRouteErrors;
- }
-
- private void ValidateActionGroupConfiguration(
- IDictionary>> methodMap,
- ControllerActionDescriptor 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(
- ActionModel 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(
- ControllerActionDescriptor 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(
- ControllerActionDescriptor 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(ActionModel 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);
- }
- }
- }
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs
deleted file mode 100644
index a67d1cd8d9..0000000000
--- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs
+++ /dev/null
@@ -1,266 +0,0 @@
-// 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.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using Microsoft.AspNet.Mvc.Routing;
-
-namespace Microsoft.AspNet.Mvc
-{
- public class DefaultActionDiscoveryConventions : IActionDiscoveryConventions
- {
- public virtual bool IsController([NotNull] TypeInfo typeInfo)
- {
- if (!typeInfo.IsClass ||
- typeInfo.IsAbstract ||
-
- // We only consider public top-level classes as controllers. IsPublic returns false for nested
- // classes, regardless of visibility modifiers.
- !typeInfo.IsPublic ||
- typeInfo.ContainsGenericParameters)
- {
- return false;
- }
-
- if (typeInfo.Name.Equals("Controller", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- return typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ||
- typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo);
- }
-
- // 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, AttributeRoute = null }}
- public virtual IEnumerable GetActions(
- [NotNull] MethodInfo methodInfo,
- [NotNull] TypeInfo controllerTypeInfo)
- {
- if (!IsValidActionMethod(methodInfo))
- {
- return null;
- }
-
- var attributes = GetActionCustomAttributes(methodInfo);
- var actionInfos = GetActionsForMethodsWithCustomAttributes(attributes, methodInfo, controllerTypeInfo);
- if (actionInfos.Any())
- {
- return actionInfos;
- }
- else
- {
- // By default the action is just matched by name.
- actionInfos = new ActionInfo[]
- {
- new ActionInfo()
- {
- ActionName = methodInfo.Name,
- Attributes = attributes.Attributes,
- RequireActionNameMatch = true,
- }
- };
- }
-
- return actionInfos;
- }
-
- ///
- /// Determines whether the method is a valid action.
- ///
- /// The .
- /// true if the method is a valid action. Otherwise, false.
- public virtual bool IsValidActionMethod(MethodInfo method)
- {
- return
- method.IsPublic &&
- !method.IsStatic &&
- !method.IsAbstract &&
- !method.IsConstructor &&
- !method.IsGenericMethod &&
-
- // The SpecialName bit is set to flag members that are treated in a special way by some compilers
- // (such as property accessors and operator overloading methods).
- !method.IsSpecialName &&
- !method.IsDefined(typeof(NonActionAttribute)) &&
-
- // Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid.
- method.GetBaseDefinition().DeclaringType != typeof(object);
- }
-
- private ActionAttributes GetActionCustomAttributes(MethodInfo methodInfo)
- {
- var attributes = methodInfo.GetCustomAttributes(inherit: true).OfType().ToArray();
- var actionNameAttribute = attributes.OfType().FirstOrDefault();
- var httpMethodConstraints = attributes.OfType();
- var routeTemplates = attributes.OfType();
-
- return new ActionAttributes()
- {
- Attributes = attributes,
- ActionNameAttribute = actionNameAttribute,
- HttpMethodProviderAttributes = httpMethodConstraints,
- RouteTemplateProviderAttributes = routeTemplates,
- };
- }
-
- private IEnumerable GetActionsForMethodsWithCustomAttributes(
- ActionAttributes actionAttributes,
- MethodInfo methodInfo,
- TypeInfo controller)
- {
- var hasControllerAttributeRoutes = HasValidControllerRouteTemplates(controller);
-
- // 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.HasSpecialAttribute() || 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.
- return Enumerable.Empty();
- }
- }
-
- 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();
- if (httpMethods.Length > 0)
- {
- foreach (var httpMethod in httpMethods)
- {
- yield return new ActionInfo()
- {
- HttpMethods = new string[] { httpMethod },
- ActionName = actionName,
- Attributes = actionAttributes.Attributes,
- RequireActionNameMatch = true,
- };
- }
- }
- else
- {
- yield return new ActionInfo()
- {
- HttpMethods = httpMethods,
- ActionName = actionName,
- Attributes = actionAttributes.Attributes,
- 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
- {
- Attributes = actionAttributes.Attributes,
- ActionName = actionName,
- HttpMethods = null,
- RequireActionNameMatch = true,
- AttributeRoute = null
- });
- }
-
- foreach (var routeTemplateProvider in actionAttributes.RouteTemplateProviderAttributes)
- {
- // We want to exclude the attributes from the other route template providers;
- var attributes = actionAttributes.Attributes
- .Where(a => a == routeTemplateProvider || !(a is IRouteTemplateProvider))
- .ToArray();
-
- actions.Add(new ActionInfo()
- {
- Attributes = attributes,
- 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 class ActionAttributes
- {
- public ActionNameAttribute ActionNameAttribute { get; set; }
-
- public object[] Attributes { get; set; }
-
- public IEnumerable HttpMethodProviderAttributes { get; set; }
- public IEnumerable RouteTemplateProviderAttributes { get; set; }
-
- public bool HasSpecialAttribute()
- {
- return ActionNameAttribute != null ||
- HttpMethodProviderAttributes.Any() ||
- RouteTemplateProviderAttributes.Any();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/IActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc.Core/IActionDiscoveryConventions.cs
deleted file mode 100644
index b3a19a30b2..0000000000
--- a/src/Microsoft.AspNet.Mvc.Core/IActionDiscoveryConventions.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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.
-
-using System.Collections.Generic;
-using System.Reflection;
-
-namespace Microsoft.AspNet.Mvc
-{
- public interface IActionDiscoveryConventions
- {
- bool IsController(TypeInfo typeInfo);
-
- IEnumerable GetActions(MethodInfo methodInfo, TypeInfo controllerTypeInfo);
- }
-}
diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs
index 1a6ea9301c..f0c3d0af31 100644
--- a/src/Microsoft.AspNet.Mvc/MvcServices.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcServices.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.ApplicationModel;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.Internal;
@@ -44,9 +45,17 @@ namespace Microsoft.AspNet.Mvc
//
// Core action discovery, filters and action execution.
//
- yield return describe.Transient();
+ // These are consumed only when creating action descriptors, then they can be de-allocated
+ yield return describe.Transient();
+ yield return describe.Transient();
+
+ // This accesses per-request services to activate the controller
yield return describe.Transient();
+
+ // This has a cache, so it needs to be a singleton
yield return describe.Singleton();
+
+ // This accesses per-reqest services
yield return describe.Transient();
// This provider needs access to the per-request services, but might be used many times for a given
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs
deleted file mode 100644
index 316d0ed481..0000000000
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs
+++ /dev/null
@@ -1,322 +0,0 @@
-// 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.
-
-#if ASPNET50
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Design;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-using Microsoft.AspNet.Http;
-using Microsoft.AspNet.Mvc.Routing;
-using Microsoft.AspNet.Routing;
-using Microsoft.Framework.DependencyInjection;
-using Microsoft.Framework.DependencyInjection.NestedProviders;
-using Moq;
-using Xunit;
-
-namespace Microsoft.AspNet.Mvc
-{
- public class ActionAttributeTests
- {
- [Theory]
- [InlineData("GET")]
- [InlineData("PUT")]
- [InlineData("POST")]
- [InlineData("DELETE")]
- [InlineData("PATCH")]
- public async Task HttpMethodAttribute_ActionWithMultipleHttpMethodAttributeViaAcceptVerbs_ORsMultipleHttpMethods(string verb)
- {
- // Arrange
- var routeContext = new RouteContext(GetHttpContext(verb));
- routeContext.RouteData.Values = new Dictionary
- {
- { "controller", "HttpMethodAttributeTests_RestOnly" },
- { "action", "Patch" }
- };
-
- // Act
- var result = await InvokeActionSelector(routeContext);
-
- // Assert
- Assert.Equal("Patch", result.Name);
- }
-
- [Theory]
- [InlineData("GET")]
- [InlineData("PUT")]
- [InlineData("POST")]
- [InlineData("DELETE")]
- [InlineData("PATCH")]
- public async Task HttpMethodAttribute_ActionWithMultipleHttpMethodAttributes_ORsMultipleHttpMethods(string verb)
- {
- // Arrange
- var routeContext = new RouteContext(GetHttpContext(verb));
- routeContext.RouteData.Values = new Dictionary()
- {
- { "controller", "HttpMethodAttributeTests_RestOnly" },
- { "action", "Put" }
- };
-
- // Act
- var result = await InvokeActionSelector(routeContext);
-
- // Assert
- Assert.Equal("Put", result.Name);
- }
-
- [Theory]
- [InlineData("GET")]
- [InlineData("PUT")]
- public async Task HttpMethodAttribute_ActionDecoratedWithHttpMethodAttribute_OverridesConvention(string verb)
- {
- // Arrange
- // Note no action name is passed, hence should return a null action descriptor.
- var routeContext = new RouteContext(GetHttpContext(verb));
- routeContext.RouteData.Values = new Dictionary()
- {
- { "controller", "HttpMethodAttributeTests_RestOnly" },
- };
-
- // Act
- var result = await InvokeActionSelector(routeContext);
-
- // Assert
- Assert.Equal(null, result);
- }
-
- [Theory]
- [InlineData("Put")]
- [InlineData("RPCMethod")]
- [InlineData("RPCMethodWithHttpGet")]
- public void NonActionAttribute_ActionNotReachable(string actionName)
- {
- // Arrange
- var actionDescriptorProvider = GetActionDescriptorProvider();
-
- // Act
- var result = actionDescriptorProvider.GetDescriptors()
- .Select(x => x as ControllerActionDescriptor)
- .FirstOrDefault(
- x=> x.ControllerName == "NonAction" &&
- x.Name == actionName);
-
- // Assert
- Assert.Null(result);
- }
-
- [Theory]
- [InlineData("GET")]
- [InlineData("PUT")]
- [InlineData("POST")]
- [InlineData("DELETE")]
- [InlineData("PATCH")]
- public async Task ActionNameAttribute_ActionGetsExposedViaActionName_UnreachableByConvention(string verb)
- {
- // Arrange
- var routeContext = new RouteContext(GetHttpContext(verb));
- routeContext.RouteData.Values = new Dictionary
- {
- { "controller", "ActionName" },
- { "action", "RPCMethodWithHttpGet" }
- };
-
- // Act
- var result = await InvokeActionSelector(routeContext);
-
- // Assert
- Assert.Equal(null, result);
- }
-
- [Theory]
- [InlineData("GET", "CustomActionName_Verb")]
- [InlineData("PUT", "CustomActionName_Verb")]
- [InlineData("POST", "CustomActionName_Verb")]
- [InlineData("DELETE", "CustomActionName_Verb")]
- [InlineData("PATCH", "CustomActionName_Verb")]
- [InlineData("GET", "CustomActionName_DefaultMethod")]
- [InlineData("PUT", "CustomActionName_DefaultMethod")]
- [InlineData("POST", "CustomActionName_DefaultMethod")]
- [InlineData("DELETE", "CustomActionName_DefaultMethod")]
- [InlineData("PATCH", "CustomActionName_DefaultMethod")]
- [InlineData("GET", "CustomActionName_RpcMethod")]
- [InlineData("PUT", "CustomActionName_RpcMethod")]
- [InlineData("POST", "CustomActionName_RpcMethod")]
- [InlineData("DELETE", "CustomActionName_RpcMethod")]
- [InlineData("PATCH", "CustomActionName_RpcMethod")]
- public async Task ActionNameAttribute_DifferentActionName_UsesActionNameFromActionNameAttribute(string verb, string actionName)
- {
- // Arrange
- var routeContext = new RouteContext(GetHttpContext(verb));
- routeContext.RouteData.Values = new Dictionary
- {
- { "controller", "ActionName" },
- { "action", actionName }
- };
-
- // Act
- var result = await InvokeActionSelector(routeContext);
-
- // Assert
- Assert.Equal(actionName, result.Name);
- }
-
- private async Task InvokeActionSelector(
- RouteContext context,
- IActionDiscoveryConventions actionDiscoveryConventions = null)
- {
- var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
- var descriptorProvider =
- new NestedProviderManager(new[] { actionDescriptorProvider });
-
- var serviceContainer = new ServiceContainer();
- serviceContainer.AddService(typeof(INestedProviderManager),
- descriptorProvider);
-
- var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
- var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
-
- var actionConstraintProvider = new NestedProviderManager(
- new INestedProvider[]
- {
- new DefaultActionConstraintProvider(serviceContainer),
- });
-
- var defaultActionSelector = new DefaultActionSelector(
- actionCollectionDescriptorProvider,
- decisionTreeProvider,
- actionConstraintProvider,
- NullLoggerFactory.Instance);
-
- return await defaultActionSelector.SelectAsync(context);
- }
-
- private ControllerActionDescriptorProvider GetActionDescriptorProvider(
- IActionDiscoveryConventions actionDiscoveryConventions = null)
- {
- var assemblyProvider = new StaticAssemblyProvider();
-
- if (actionDiscoveryConventions == null)
- {
- var controllerTypes = typeof(ActionAttributeTests)
- .GetNestedTypes(BindingFlags.NonPublic)
- .Select(t => t.GetTypeInfo());
-
- actionDiscoveryConventions = new StaticActionDiscoveryConventions(controllerTypes.ToArray());
- }
-
- return new ControllerActionDescriptorProvider(
- assemblyProvider,
- actionDiscoveryConventions,
- new TestGlobalFilterProvider(),
- new MockMvcOptionsAccessor());
- }
-
- private static HttpContext GetHttpContext(string httpMethod)
- {
- var request = new Mock();
- var headers = new Mock();
- request.SetupGet(r => r.Headers).Returns(headers.Object);
- request.SetupGet(x => x.Method).Returns(httpMethod);
- var httpContext = new Mock();
- httpContext.SetupGet(c => c.Request).Returns(request.Object);
- return httpContext.Object;
- }
-
- private class CustomActionConvention : DefaultActionDiscoveryConventions
- {
- public override IEnumerable GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo)
- {
- var actions = new List(base.GetActions(methodInfo, controllerTypeInfo));
- if (methodInfo.Name == "PostSomething")
- {
- actions[0].HttpMethods = new string[] { "POST" };
- }
-
- return actions;
- }
- }
-
- #region Controller Classes
-
- private class NonActionController
- {
- [NonAction]
- public void Put()
- {
- }
-
- [NonAction]
- public void RPCMethod()
- {
- }
-
- [NonAction]
- [HttpGet]
- public void RPCMethodWithHttpGet()
- {
- }
- }
-
- private class HttpMethodAttributeTests_DefaultMethodValidationController
- {
- public void Index()
- {
- }
-
- // Method with custom attribute.
- [HttpGet]
- public void Get()
- { }
-
- // InvalidMethod ( since its private)
- private void Post()
- { }
- }
-
- private class ActionNameController
- {
- [ActionName("CustomActionName_Verb")]
- public void Put()
- {
- }
-
- [ActionName("CustomActionName_DefaultMethod")]
- public void Index()
- {
- }
-
- [ActionName("CustomActionName_RpcMethod")]
- public void RPCMethodWithHttpGet()
- {
- }
- }
-
- private class HttpMethodAttributeTests_RestOnlyController
- {
- [HttpGet]
- [HttpPut]
- [HttpPost]
- [HttpDelete]
- [HttpPatch]
- public void Put()
- {
- }
-
- [AcceptVerbs("PUT", "post", "GET", "delete", "pATcH")]
- public void Patch()
- {
- }
- }
-
- private class HttpMethodAttributeTests_DerivedController : HttpMethodAttributeTests_RestOnlyController
- {
- }
-
- #endregion Controller Classes
- }
-}
-
-#endif
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultActionModelBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultActionModelBuilderTest.cs
new file mode 100644
index 0000000000..19248753f7
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultActionModelBuilderTest.cs
@@ -0,0 +1,603 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.ApplicationModel
+{
+ public class DefaultActionModelBuilderTest
+ {
+ [Theory]
+ [InlineData("GetFromDerived", true)]
+ [InlineData("NewMethod", true)] // "NewMethod" is a public method declared with keyword "new".
+ [InlineData("GetFromBase", true)]
+ public void IsAction_WithInheritedMethods(string methodName, bool expected)
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(DerivedController).GetMethod(methodName);
+ Assert.NotNull(method);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.Equal(expected, isValid);
+ }
+
+ [Fact]
+ public void IsAction_OverridenMethodControllerClass()
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(BaseController).GetMethod(nameof(BaseController.Redirect));
+ Assert.NotNull(method);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsAction_PrivateMethod_FromUserDefinedController()
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(DerivedController).GetMethod(
+ "PrivateMethod",
+ BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
+ Assert.NotNull(method);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsAction_OperatorOverloadingMethod_FromOperatorOverloadingController()
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(OperatorOverloadingController).GetMethod("op_Addition");
+ Assert.NotNull(method);
+ Assert.True(method.IsSpecialName);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsAction_GenericMethod_FromUserDefinedController()
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(DerivedController).GetMethod("GenericMethod");
+ Assert.NotNull(method);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsAction_OverridenNonActionMethod()
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(DerivedController).GetMethod("OverridenNonActionMethod");
+ Assert.NotNull(method);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Theory]
+ [InlineData("Equals")]
+ [InlineData("GetHashCode")]
+ [InlineData("MemberwiseClone")]
+ [InlineData("ToString")]
+ public void IsAction_OverriddenMethodsFromObjectClass(string methodName)
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(DerivedController).GetMethod(
+ methodName,
+ BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
+ Assert.NotNull(method);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Theory]
+ [InlineData("StaticMethod")]
+ [InlineData("ProtectedStaticMethod")]
+ [InlineData("PrivateStaticMethod")]
+ public void IsAction_StaticMethods(string methodName)
+ {
+ // Arrange
+ var builder = new AccessibleActionModelBuilder();
+ var method = typeof(DerivedController).GetMethod(
+ methodName,
+ BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ Assert.NotNull(method);
+
+ // Act
+ var isValid = builder.IsAction(method);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void GetActions_ConventionallyRoutedAction_WithoutHttpConstraints()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
+ var actionName = nameof(ConventionallyRoutedController.Edit);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+ Assert.Equal("Edit", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+ Assert.Empty(action.HttpMethods);
+ Assert.Null(action.AttributeRouteModel);
+ Assert.Empty(action.Attributes);
+ }
+
+ [Fact]
+ public void GetActions_ConventionallyRoutedAction_WithHttpConstraints()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
+ var actionName = nameof(ConventionallyRoutedController.Update);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+ Assert.Contains("PUT", action.HttpMethods);
+ Assert.Contains("PATCH", action.HttpMethods);
+
+ Assert.Equal("Update", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+ Assert.Null(action.AttributeRouteModel);
+ Assert.IsType(Assert.Single(action.Attributes));
+ }
+
+ [Fact]
+ public void GetActions_ConventionallyRoutedActionWithHttpConstraints_AndInvalidRouteTemplateProvider()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
+ var actionName = nameof(ConventionallyRoutedController.Delete);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+ Assert.Equal("Delete", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ var httpMethod = Assert.Single(action.HttpMethods);
+ Assert.Equal("DELETE", httpMethod);
+ Assert.Null(action.AttributeRouteModel);
+
+ Assert.IsType(Assert.Single(action.Attributes));
+ }
+
+ [Fact]
+ public void GetActions_ConventionallyRoutedAction_WithMultipleHttpConstraints()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
+ var actionName = nameof(ConventionallyRoutedController.Details);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+ Assert.Contains("GET", action.HttpMethods);
+ Assert.Contains("POST", action.HttpMethods);
+ Assert.Equal("Details", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+ Assert.Null(action.AttributeRouteModel);
+ }
+
+ [Fact]
+ public void GetActions_ConventionallyRoutedAction_WithMultipleOverlappingHttpConstraints()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
+ var actionName = nameof(ConventionallyRoutedController.List);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+ Assert.Contains("GET", action.HttpMethods);
+ Assert.Contains("PUT", action.HttpMethods);
+ Assert.Contains("POST", action.HttpMethods);
+ Assert.Equal("List", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+ Assert.Null(action.AttributeRouteModel);
+ }
+
+ [Fact]
+ public void GetActions_AttributeRouteOnAction()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
+ var actionName = nameof(NoRouteAttributeOnControllerController.Edit);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+
+ Assert.Equal("Edit", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ var httpMethod = Assert.Single(action.HttpMethods);
+ Assert.Equal("POST", httpMethod);
+
+ Assert.NotNull(action.AttributeRouteModel);
+ Assert.Equal("Change", action.AttributeRouteModel.Template);
+
+ Assert.IsType(Assert.Single(action.Attributes));
+ }
+
+ [Fact]
+ public void GetActions_AttributeRouteOnAction_RouteAttribute()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
+ var actionName = nameof(NoRouteAttributeOnControllerController.Update);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+
+ Assert.Equal("Update", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ Assert.Empty(action.HttpMethods);
+
+ Assert.NotNull(action.AttributeRouteModel);
+ Assert.Equal("Update", action.AttributeRouteModel.Template);
+
+ Assert.IsType(Assert.Single(action.Attributes));
+ }
+
+ [Fact]
+ public void GetActions_AttributeRouteOnAction_AcceptVerbsAttributeWithTemplate()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
+ var actionName = nameof(NoRouteAttributeOnControllerController.List);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+
+ Assert.Equal("List", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ Assert.Equal(new[] { "GET", "HEAD" }, action.HttpMethods.OrderBy(m => m, StringComparer.Ordinal));
+
+ Assert.NotNull(action.AttributeRouteModel);
+ Assert.Equal("ListAll", action.AttributeRouteModel.Template);
+
+ Assert.IsType(Assert.Single(action.Attributes));
+ }
+
+ [Fact]
+ public void GetActions_AttributeRouteOnAction_CreatesOneActionInforPerRouteTemplate()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
+ var actionName = nameof(NoRouteAttributeOnControllerController.Index);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ Assert.Equal(2, actions.Count());
+
+ foreach (var action in actions)
+ {
+ Assert.Equal("Index", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ Assert.NotNull(action.AttributeRouteModel);
+ }
+
+ var list = Assert.Single(actions, ai => ai.AttributeRouteModel.Template.Equals("List"));
+ var listMethod = Assert.Single(list.HttpMethods);
+ Assert.Equal("POST", listMethod);
+ Assert.IsType(Assert.Single(list.Attributes));
+
+ var all = Assert.Single(actions, ai => ai.AttributeRouteModel.Template.Equals("All"));
+ var allMethod = Assert.Single(all.HttpMethods);
+ Assert.Equal("GET", allMethod);
+ Assert.IsType(Assert.Single(all.Attributes));
+ }
+
+ [Fact]
+ public void GetActions_NoRouteOnController_AllowsConventionallyRoutedActions_OnTheSameController()
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
+ var actionName = nameof(NoRouteAttributeOnControllerController.Remove);
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod(actionName));
+
+ // Assert
+ var action = Assert.Single(actions);
+
+ Assert.Equal("Remove", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ Assert.Empty(action.HttpMethods);
+
+ Assert.Null(action.AttributeRouteModel);
+
+ Assert.Empty(action.Attributes);
+ }
+
+ [Theory]
+ [InlineData(typeof(SingleRouteAttributeController))]
+ [InlineData(typeof(MultipleRouteAttributeController))]
+ public void GetActions_RouteAttributeOnController_CreatesAttributeRoute_ForNonAttributedActions(Type controller)
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = controller.GetTypeInfo();
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod("Delete"));
+
+ // Assert
+ var action = Assert.Single(actions);
+
+ Assert.Equal("Delete", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ Assert.Empty(action.HttpMethods);
+
+ Assert.Null(action.AttributeRouteModel);
+
+ Assert.Empty(action.Attributes);
+ }
+
+ [Theory]
+ [InlineData(typeof(SingleRouteAttributeController))]
+ [InlineData(typeof(MultipleRouteAttributeController))]
+ public void GetActions_RouteOnController_CreatesOneActionInforPerRouteTemplateOnAction(Type controller)
+ {
+ // Arrange
+ var builder = new DefaultActionModelBuilder();
+ var typeInfo = controller.GetTypeInfo();
+
+ // Act
+ var actions = builder.BuildActionModels(typeInfo.GetMethod("Index"));
+
+ // Assert
+ Assert.Equal(2, actions.Count());
+
+ foreach (var action in actions)
+ {
+ Assert.Equal("Index", action.ActionName);
+ Assert.True(action.IsActionNameMatchRequired);
+
+ var httpMethod = Assert.Single(action.HttpMethods);
+ Assert.Equal("GET", httpMethod);
+
+ Assert.NotNull(action.AttributeRouteModel.Template);
+
+ Assert.IsType(Assert.Single(action.Attributes));
+ }
+
+ Assert.Single(actions, ai => ai.AttributeRouteModel.Template.Equals("List"));
+ Assert.Single(actions, ai => ai.AttributeRouteModel.Template.Equals("All"));
+ }
+
+ private class AccessibleActionModelBuilder : DefaultActionModelBuilder
+ {
+ public new bool IsAction([NotNull]MethodInfo methodInfo)
+ {
+ return base.IsAction(methodInfo);
+ }
+ }
+
+ private class BaseController : Controller
+ {
+ public void GetFromBase() // Valid action method.
+ {
+ }
+
+ [NonAction]
+ public virtual void OverridenNonActionMethod()
+ {
+ }
+
+ [NonAction]
+ public virtual void NewMethod()
+ {
+ }
+
+ public override RedirectResult Redirect(string url)
+ {
+ return base.Redirect(url + "#RedirectOverride");
+ }
+ }
+
+ private class DerivedController : BaseController
+ {
+ public void GetFromDerived() // Valid action method.
+ {
+ }
+
+ [HttpGet]
+ public override void OverridenNonActionMethod()
+ {
+ }
+
+ public new void NewMethod() // Valid action method.
+ {
+ }
+
+ public void GenericMethod()
+ {
+ }
+
+ private void PrivateMethod()
+ {
+ }
+
+ public static void StaticMethod()
+ {
+ }
+
+ protected static void ProtectedStaticMethod()
+ {
+ }
+
+ private static void PrivateStaticMethod()
+ {
+ }
+ }
+
+ private class OperatorOverloadingController : Mvc.Controller
+ {
+ public static OperatorOverloadingController operator +(
+ OperatorOverloadingController c1,
+ OperatorOverloadingController c2)
+ {
+ return new OperatorOverloadingController();
+ }
+ }
+
+ private class NoRouteAttributeOnControllerController : Controller
+ {
+ [HttpGet("All")]
+ [HttpPost("List")]
+ public void Index() { }
+
+ [HttpPost("Change")]
+ public void Edit() { }
+
+ public void Remove() { }
+
+ [Route("Update")]
+ public void Update() { }
+
+ [AcceptVerbs("GET", "HEAD", Route = "ListAll")]
+ public void List() { }
+ }
+
+ [Route("Products")]
+ private class SingleRouteAttributeController : Controller
+ {
+ [HttpGet("All")]
+ [HttpGet("List")]
+ public void Index() { }
+
+ public void Delete() { }
+ }
+
+ [Route("Products")]
+ [Route("Items")]
+ private class MultipleRouteAttributeController : Controller
+ {
+ [HttpGet("All")]
+ [HttpGet("List")]
+ public void Index() { }
+
+ public void Delete() { }
+ }
+
+ // Here the constraints on the methods are acting as an IActionHttpMethodProvider and
+ // not as an IRouteTemplateProvider given that there is no RouteAttribute
+ // on the controller and the template for all the constraints on a method is null.
+ private class ConventionallyRoutedController : Controller
+ {
+ public void Edit() { }
+
+ [CustomHttpMethods("PUT", "PATCH")]
+ public void Update() { }
+
+ [HttpDelete]
+ public void Delete() { }
+
+ [HttpPost]
+ [HttpGet]
+ public void Details() { }
+
+ [HttpGet]
+ [HttpPut]
+ [AcceptVerbs("GET", "POST")]
+ public void List() { }
+ }
+
+ private class CustomHttpMethodsAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private readonly string[] _methods;
+
+ public CustomHttpMethodsAttribute(params string[] methods)
+ {
+ _methods = methods;
+ }
+
+ public IEnumerable HttpMethods
+ {
+ get
+ {
+ return _methods;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs
new file mode 100644
index 0000000000..6843a0aff6
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs
@@ -0,0 +1,223 @@
+// 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.
+
+using System.Reflection;
+using Microsoft.AspNet.Mvc.ApplicationModel.DefaultControllerModelBuilderTestControllers;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.ApplicationModel
+{
+ public class DefaultControllerModelBuilderTest
+ {
+ [Fact]
+ public void IsController_UserDefinedClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(StoreController).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.True(isController);
+ }
+
+ [Fact]
+ public void IsController_FrameworkControllerClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(Controller).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.False(isController);
+ }
+
+ [Fact]
+ public void IsController_UserDefinedControllerClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(DefaultControllerModelBuilderTestControllers.Controller).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.False(isController);
+ }
+
+ [Fact]
+ public void IsController_Interface()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(IController).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.False(isController);
+ }
+
+ [Fact]
+ public void IsController_AbstractClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(AbstractController).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.False(isController);
+ }
+
+ [Fact]
+ public void IsController_DerivedAbstractClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(DerivedAbstractController).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.True(isController);
+ }
+
+ [Fact]
+ public void IsController_OpenGenericClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(OpenGenericController<>).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.False(isController);
+ }
+
+ [Fact]
+ public void IsController_ClosedGenericClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(OpenGenericController).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.False(isController);
+ }
+
+ [Fact]
+ public void IsController_DerivedGenericClass()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(DerivedGenericController).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.True(isController);
+ }
+
+ [Fact]
+ public void IsController_Poco_WithNamingConvention()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(PocoController).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.True(isController);
+ }
+
+ [Fact]
+ public void IsController_NoControllerSuffix()
+ {
+ // Arrange
+ var builder = new AccessibleControllerModelBuilder();
+ var typeInfo = typeof(NoSuffix).GetTypeInfo();
+
+ // Act
+ var isController = builder.IsController(typeInfo);
+
+ // Assert
+ Assert.True(isController);
+ }
+
+ private class AccessibleControllerModelBuilder : DefaultControllerModelBuilder
+ {
+ public AccessibleControllerModelBuilder()
+ : base(new DefaultActionModelBuilder())
+ {
+ }
+
+ public new bool IsController([NotNull]TypeInfo typeInfo)
+ {
+ return base.IsController(typeInfo);
+ }
+ }
+ }
+}
+
+// These controllers are used to test the DefaultActionDiscoveryConventions implementation
+// which REQUIRES that they be public top-level classes. To avoid having to stub out the
+// implementation of this class to test it, they are just top level classes. Don't reuse
+// these outside this test - find a better way or use nested classes to keep the tests
+// independent.
+namespace Microsoft.AspNet.Mvc.ApplicationModel.DefaultControllerModelBuilderTestControllers
+{
+ public abstract class AbstractController : Mvc.Controller
+ {
+ }
+
+ public class DerivedAbstractController : AbstractController
+ {
+ }
+
+ public class StoreController : Mvc.Controller
+ {
+ }
+
+ public class Controller
+ {
+ }
+
+ public class OpenGenericController
+ {
+ }
+
+ public class DerivedGenericController : OpenGenericController
+ {
+ }
+
+ public interface IController
+ {
+ }
+
+ public class NoSuffix : Mvc.Controller
+ {
+ }
+
+ public class PocoController
+ {
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs
index ec041df64f..9b1079dc1e 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs
@@ -767,8 +767,6 @@ namespace Microsoft.AspNet.Mvc.Test
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' - Template: 'AttributeRouted'" + Environment.NewLine +
"Action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" +
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' - Template: '(none)'" + Environment.NewLine +
- "Action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" +
- "AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' - Template: '(none)'" + Environment.NewLine +
"A method 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" +
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' that defines attribute routed actions must not" +
" have attributes that implement 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' and do not implement" +
@@ -1153,7 +1151,7 @@ namespace Microsoft.AspNet.Mvc.Test
parameter.Attributes.Add(parameterConvention.Object);
// Act
- provider.ApplyConventions(model);
+ ApplicationModelConventions.ApplyConventions(model, options.Options.ApplicationModelConventions);
// Assert
Assert.Equal(4, sequence);
@@ -1306,7 +1304,7 @@ namespace Microsoft.AspNet.Mvc.Test
TypeInfo controllerTypeInfo,
IEnumerable filters = null)
{
- var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo);
+ var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfo);
var assemblyProvider = new Mock();
assemblyProvider
@@ -1315,7 +1313,7 @@ namespace Microsoft.AspNet.Mvc.Test
var provider = new ControllerActionDescriptorProvider(
assemblyProvider.Object,
- conventions,
+ modelBuilder,
new TestGlobalFilterProvider(filters),
new MockMvcOptionsAccessor());
@@ -1325,7 +1323,7 @@ namespace Microsoft.AspNet.Mvc.Test
private ControllerActionDescriptorProvider GetProvider(
params TypeInfo[] controllerTypeInfo)
{
- var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo);
+ var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfo);
var assemblyProvider = new Mock();
assemblyProvider
@@ -1334,7 +1332,7 @@ namespace Microsoft.AspNet.Mvc.Test
var provider = new ControllerActionDescriptorProvider(
assemblyProvider.Object,
- conventions,
+ modelBuilder,
new TestGlobalFilterProvider(),
new MockMvcOptionsAccessor());
@@ -1345,7 +1343,7 @@ namespace Microsoft.AspNet.Mvc.Test
TypeInfo type,
IOptions options)
{
- var conventions = new StaticActionDiscoveryConventions(type);
+ var modelBuilder = new StaticControllerModelBuilder(type);
var assemblyProvider = new Mock();
assemblyProvider
@@ -1354,14 +1352,14 @@ namespace Microsoft.AspNet.Mvc.Test
return new ControllerActionDescriptorProvider(
assemblyProvider.Object,
- conventions,
+ modelBuilder,
new TestGlobalFilterProvider(),
options);
}
private IEnumerable GetDescriptors(params TypeInfo[] controllerTypeInfos)
{
- var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos);
+ var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfos);
var assemblyProvider = new Mock();
assemblyProvider
@@ -1370,7 +1368,7 @@ namespace Microsoft.AspNet.Mvc.Test
var provider = new ControllerActionDescriptorProvider(
assemblyProvider.Object,
- conventions,
+ modelBuilder,
new TestGlobalFilterProvider(),
new MockMvcOptionsAccessor());
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs
deleted file mode 100644
index de2e369f1c..0000000000
--- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-// 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.
-
-#if ASPNET50
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Design;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-using Microsoft.AspNet.Http;
-using Microsoft.AspNet.Mvc.Routing;
-using Microsoft.AspNet.Routing;
-using Microsoft.Framework.DependencyInjection;
-using Microsoft.Framework.DependencyInjection.NestedProviders;
-using Moq;
-using Xunit;
-
-namespace Microsoft.AspNet.Mvc
-{
- public class DefaultActionDiscoveryConventionsActionSelectionTests
- {
- [Fact]
- public async Task ActionSelection_ActionSelectedByName()
- {
- // Arrange
- var routeContext = new RouteContext(GetHttpContext("GET"));
- routeContext.RouteData.Values = new Dictionary
- {
- { "controller", "RpcOnly" },
- { "action", "Index" }
- };
-
- // Act
- var result = await InvokeActionSelector(routeContext);
-
- // Assert
- Assert.Equal("Index", result.Name);
- }
-
- // Uses custom conventions to map a web-api-style action
- [Fact]
- public async Task ActionSelection_ChangeDefaultConventionPicksCustomMethodForPost_CutomMethodIsSelected()
- {
- // Arrange
- var routeContext = new RouteContext(GetHttpContext("POST"));
- routeContext.RouteData.Values = new Dictionary
- {
- { "controller", "RpcOnly" }
- };
-
- // Act
- var result = await InvokeActionSelector(routeContext, new CustomActionConvention());
-
- // Assert
- Assert.Equal("PostSomething", result.Name);
- }
-
- private async Task InvokeActionSelector(RouteContext context)
- {
- var controllerTypeInfos = typeof(DefaultActionDiscoveryConventionsActionSelectionTests)
- .GetNestedTypes(BindingFlags.NonPublic)
- .Select(ct => ct.GetTypeInfo())
- .ToArray();
-
- var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos);
- return await InvokeActionSelector(context, conventions);
- }
-
- private async Task InvokeActionSelector(RouteContext context,
- DefaultActionDiscoveryConventions actionDiscoveryConventions)
- {
- var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
- var descriptorProvider =
- new NestedProviderManager(new[] { actionDescriptorProvider });
-
- var serviceContainer = new ServiceContainer();
- serviceContainer.AddService(typeof(INestedProviderManager),
- descriptorProvider);
-
- var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
- var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
-
- var actionConstraintProvider = new NestedProviderManager(
- new INestedProvider[]
- {
- new DefaultActionConstraintProvider(serviceContainer),
- });
-
- var defaultActionSelector = new DefaultActionSelector(
- actionCollectionDescriptorProvider,
- decisionTreeProvider,
- actionConstraintProvider,
- NullLoggerFactory.Instance);
-
- return await defaultActionSelector.SelectAsync(context);
- }
-
- private ControllerActionDescriptorProvider GetActionDescriptorProvider(DefaultActionDiscoveryConventions actionDiscoveryConventions)
- {
- var assemblies = new Assembly[] { typeof(DefaultActionDiscoveryConventionsActionSelectionTests).GetTypeInfo().Assembly, };
- var AssemblyProvider = new Mock();
- AssemblyProvider.SetupGet(x => x.CandidateAssemblies).Returns(assemblies);
- return new ControllerActionDescriptorProvider(
- AssemblyProvider.Object,
- actionDiscoveryConventions,
- new TestGlobalFilterProvider(),
- new MockMvcOptionsAccessor());
- }
-
- private static HttpContext GetHttpContext(string httpMethod)
- {
- var request = new Mock();
- var headers = new Mock();
- request.SetupGet(r => r.Headers).Returns(headers.Object);
- request.SetupGet(x => x.Method).Returns(httpMethod);
- var httpContext = new Mock();
- httpContext.SetupGet(c => c.Request).Returns(request.Object);
- return httpContext.Object;
- }
-
- private class CustomActionConvention : DefaultActionDiscoveryConventions
- {
- public override bool IsController([NotNull]TypeInfo typeInfo)
- {
- return
- typeof(DefaultActionDiscoveryConventionsActionSelectionTests)
- .GetNestedTypes(BindingFlags.NonPublic)
- .Select(ct => ct.GetTypeInfo())
- .Contains(typeInfo);
- }
-
- public override IEnumerable GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo)
- {
- var actions = new List(
- base.GetActions(methodInfo, controllerTypeInfo) ??
- new List());
-
- if (methodInfo.Name == "PostSomething")
- {
- actions[0].HttpMethods = new string[] { "POST" };
- actions[0].RequireActionNameMatch = false;
- }
-
- return actions;
- }
- }
-
- private class RpcOnlyController
- {
- public void Index()
- {
- }
-
- public void GetSomething()
- {
- }
-
- public void PutSomething()
- {
- }
-
- public void PostSomething()
- {
- }
-
- public void DeleteSomething()
- {
- }
-
- public void PatchSomething()
- {
- }
- }
-
- private class AmbiguousController
- {
- public void Index(int i)
- { }
-
- public void Index(string s)
- { }
- }
- }
-}
-
-#endif
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsTests.cs
deleted file mode 100644
index 0fdc895aac..0000000000
--- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsTests.cs
+++ /dev/null
@@ -1,811 +0,0 @@
-// 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.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using Microsoft.AspNet.Mvc.DefaultActionDiscoveryConventionsControllers;
-using Xunit;
-
-namespace Microsoft.AspNet.Mvc
-{
- public class DefaultActionDiscoveryConventionsTests
- {
- [Theory]
- [InlineData("GetFromDerived", true)]
- [InlineData("NewMethod", true)] // "NewMethod" is a public method declared with keyword "new".
- [InlineData("GetFromBase", true)]
- public void IsValidActionMethod_WithInheritedMethods(string methodName, bool expected)
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(DerivedController).GetMethod(methodName);
- Assert.NotNull(method);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.Equal(expected, isValid);
- }
-
- [Fact]
- public void IsValidActionMethod_OverridenMethodControllerClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(BaseController).GetMethod("Redirect");
- Assert.NotNull(method);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.False(isValid);
- }
-
- [Fact]
- public void IsValidActionMethod_PrivateMethod_FromUserDefinedController()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(DerivedController).GetMethod(
- "PrivateMethod",
- BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
- Assert.NotNull(method);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.False(isValid);
- }
-
- [Fact]
- public void IsValidActionMethod_OperatorOverloadingMethod_FromOperatorOverloadingController()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(OperatorOverloadingController).GetMethod("op_Addition");
- Assert.NotNull(method);
- Assert.True(method.IsSpecialName);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.False(isValid);
- }
-
- [Fact]
- public void IsValidActionMethod_GenericMethod_FromUserDefinedController()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(DerivedController).GetMethod("GenericMethod");
- Assert.NotNull(method);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.False(isValid);
- }
-
- [Fact]
- public void IsValidActionMethod_OverridenNonActionMethod()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(DerivedController).GetMethod("OverridenNonActionMethod");
- Assert.NotNull(method);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.False(isValid);
- }
-
- [Theory]
- [InlineData("Equals")]
- [InlineData("GetHashCode")]
- [InlineData("MemberwiseClone")]
- [InlineData("ToString")]
- public void IsValidActionMethod_OverriddenMethodsFromObjectClass(string methodName)
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(DerivedController).GetMethod(
- methodName,
- BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
- Assert.NotNull(method);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.False(isValid);
- }
-
- [Theory]
- [InlineData("StaticMethod")]
- [InlineData("ProtectedStaticMethod")]
- [InlineData("PrivateStaticMethod")]
- public void IsValidActionMethod_StaticMethods(string methodName)
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var method = typeof(DerivedController).GetMethod(
- methodName,
- BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
- Assert.NotNull(method);
-
- // Act
- var isValid = conventions.IsValidActionMethod(method);
-
- // Assert
- Assert.False(isValid);
- }
-
- [Fact]
- public void GetActions_ConventionallyRoutedAction_WithoutHttpConstraints()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
- var actionName = nameof(ConventionallyRoutedController.Edit);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- var action = Assert.Single(actionInfos);
- Assert.Equal("Edit", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
- Assert.Null(action.HttpMethods);
- Assert.Null(action.AttributeRoute);
- Assert.Empty(action.Attributes);
- }
-
- [Fact]
- public void GetActions_ConventionallyRoutedAction_WithHttpConstraints()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
- var actionName = nameof(ConventionallyRoutedController.Update);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- Assert.Equal(2, actionInfos.Count());
- Assert.Single(actionInfos, a => a.HttpMethods.Contains("PUT"));
- Assert.Single(actionInfos, a => a.HttpMethods.Contains("PATCH"));
-
- foreach (var action in actionInfos)
- {
- Assert.Equal("Update", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
- Assert.Null(action.AttributeRoute);
- Assert.IsType(Assert.Single(action.Attributes));
- }
- }
-
- [Fact]
- public void GetActions_ConventionallyRoutedActionWithHttpConstraints_AndInvalidRouteTemplateProvider()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
- var actionName = nameof(ConventionallyRoutedController.Delete);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- var action = Assert.Single(actionInfos);
- Assert.Equal("Delete", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- var httpMethod = Assert.Single(action.HttpMethods);
- Assert.Equal("DELETE", httpMethod);
- Assert.Null(action.AttributeRoute);
-
- Assert.IsType(Assert.Single(action.Attributes));
- }
-
- [Fact]
- public void GetActions_ConventionallyRoutedAction_WithMultipleHttpConstraints()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
- var actionName = nameof(ConventionallyRoutedController.Details);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- Assert.Equal(2, actionInfos.Count());
- Assert.Single(actionInfos, a => a.HttpMethods.Contains("GET"));
- Assert.Single(actionInfos, a => a.HttpMethods.Contains("POST"));
-
- foreach (var action in actionInfos)
- {
-
- Assert.Equal("Details", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
- Assert.Null(action.AttributeRoute);
-
- Assert.Equal(2, action.Attributes.Length);
- Assert.Single(action.Attributes, a => a is HttpGetAttribute);
- Assert.Single(action.Attributes, a => a is HttpPostAttribute);
- }
- }
-
- [Fact]
- public void GetActions_ConventionallyRoutedAction_WithMultipleOverlappingHttpConstraints()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
- var actionName = nameof(ConventionallyRoutedController.List);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- Assert.Equal(3, actionInfos.Count());
- Assert.Single(actionInfos, a => a.HttpMethods.Contains("GET"));
- Assert.Single(actionInfos, a => a.HttpMethods.Contains("POST"));
- Assert.Single(actionInfos, a => a.HttpMethods.Contains("PUT"));
-
- foreach (var action in actionInfos)
- {
- Assert.Equal("List", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
- Assert.Null(action.AttributeRoute);
-
- Assert.Equal(3, action.Attributes.Length);
- Assert.Single(action.Attributes, a => a is HttpPutAttribute);
- Assert.Single(action.Attributes, a => a is HttpGetAttribute);
- Assert.Single(action.Attributes, a => a is AcceptVerbsAttribute);
- }
- }
-
- [Fact]
- public void GetActions_AttributeRouteOnAction()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
- var actionName = nameof(NoRouteAttributeOnControllerController.Edit);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- var action = Assert.Single(actionInfos);
-
- Assert.Equal("Edit", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- var httpMethod = Assert.Single(action.HttpMethods);
- Assert.Equal("POST", httpMethod);
-
- Assert.NotNull(action.AttributeRoute);
- Assert.Equal("Change", action.AttributeRoute.Template);
-
- Assert.IsType(Assert.Single(action.Attributes));
- }
-
- [Fact]
- public void GetActions_AttributeRouteOnAction_RouteAttribute()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
- var actionName = nameof(NoRouteAttributeOnControllerController.Update);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- var action = Assert.Single(actionInfos);
-
- Assert.Equal("Update", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- Assert.Null(action.HttpMethods);
-
- Assert.NotNull(action.AttributeRoute);
- Assert.Equal("Update", action.AttributeRoute.Template);
-
- Assert.IsType(Assert.Single(action.Attributes));
- }
-
- [Fact]
- public void GetActions_AttributeRouteOnAction_AcceptVerbsAttributeWithTemplate()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
- var actionName = nameof(NoRouteAttributeOnControllerController.List);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- var action = Assert.Single(actionInfos);
-
- Assert.Equal("List", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- Assert.Equal(new[] { "GET", "HEAD" }, action.HttpMethods.OrderBy(m => m, StringComparer.Ordinal));
-
- Assert.NotNull(action.AttributeRoute);
- Assert.Equal("ListAll", action.AttributeRoute.Template);
-
- Assert.IsType(Assert.Single(action.Attributes));
- }
-
- [Fact]
- public void GetActions_AttributeRouteOnAction_CreatesOneActionInforPerRouteTemplate()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
- var actionName = nameof(NoRouteAttributeOnControllerController.Index);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- Assert.Equal(2, actionInfos.Count());
-
- foreach (var action in actionInfos)
- {
- Assert.Equal("Index", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- Assert.NotNull(action.AttributeRoute);
- }
-
- var list = Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("List"));
- var listMethod = Assert.Single(list.HttpMethods);
- Assert.Equal("POST", listMethod);
- Assert.IsType(Assert.Single(list.Attributes));
-
- var all = Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("All"));
- var allMethod = Assert.Single(all.HttpMethods);
- Assert.Equal("GET", allMethod);
- Assert.IsType(Assert.Single(all.Attributes));
- }
-
- [Fact]
- public void GetActions_NoRouteOnController_AllowsConventionallyRoutedActions_OnTheSameController()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
- var actionName = nameof(NoRouteAttributeOnControllerController.Remove);
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
-
- // Assert
- var action = Assert.Single(actionInfos);
-
- Assert.Equal("Remove", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- Assert.Null(action.HttpMethods);
-
- Assert.Null(action.AttributeRoute);
-
- Assert.Empty(action.Attributes);
- }
-
- [Theory]
- [InlineData(typeof(SingleRouteAttributeController))]
- [InlineData(typeof(MultipleRouteAttributeController))]
- public void GetActions_RouteAttributeOnController_CreatesAttributeRoute_ForNonAttributedActions(Type controller)
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = controller.GetTypeInfo();
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod("Delete"), typeInfo);
-
- // Assert
- var action = Assert.Single(actionInfos);
-
- Assert.Equal("Delete", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- Assert.Null(action.HttpMethods);
-
- Assert.Null(action.AttributeRoute);
-
- Assert.Empty(action.Attributes);
- }
-
- [Theory]
- [InlineData(typeof(SingleRouteAttributeController))]
- [InlineData(typeof(MultipleRouteAttributeController))]
- public void GetActions_RouteOnController_CreatesOneActionInforPerRouteTemplateOnAction(Type controller)
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = controller.GetTypeInfo();
-
- // Act
- var actionInfos = conventions.GetActions(typeInfo.GetMethod("Index"), typeInfo);
-
- // Assert
- Assert.Equal(2, actionInfos.Count());
-
- foreach (var action in actionInfos)
- {
- Assert.Equal("Index", action.ActionName);
- Assert.True(action.RequireActionNameMatch);
-
- var httpMethod = Assert.Single(action.HttpMethods);
- Assert.Equal("GET", httpMethod);
-
- Assert.NotNull(action.AttributeRoute);
-
- Assert.IsType(Assert.Single(action.Attributes));
- }
-
- Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("List"));
- Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("All"));
- }
-
- [Fact]
- public void IsController_UserDefinedClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(BaseController).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.True(isController);
- }
-
- [Fact]
- public void IsController_FrameworkControllerClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(Controller).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.False(isController);
- }
-
- [Fact]
- public void IsController_UserDefinedControllerClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(DefaultActionDiscoveryConventionsControllers.Controller).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.False(isController);
- }
-
- [Fact]
- public void IsController_Interface()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(IController).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.False(isController);
- }
-
- [Fact]
- public void IsController_AbstractClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(AbstractController).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.False(isController);
- }
-
- [Fact]
- public void IsController_DerivedAbstractClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(DerivedAbstractController).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.True(isController);
- }
-
- [Fact]
- public void IsController_OpenGenericClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(OpenGenericController<>).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.False(isController);
- }
-
- [Fact]
- public void IsController_ClosedGenericClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(OpenGenericController).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.False(isController);
- }
-
- [Fact]
- public void IsController_DerivedGenericClass()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(DerivedGenericController).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.True(isController);
- }
-
- [Fact]
- public void IsController_Poco_WithNamingConvention()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(PocoController).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.True(isController);
- }
-
- [Fact]
- public void IsController_NoControllerSuffix()
- {
- // Arrange
- var conventions = new DefaultActionDiscoveryConventions();
- var typeInfo = typeof(NoSuffix).GetTypeInfo();
-
- // Act
- var isController = conventions.IsController(typeInfo);
-
- // Assert
- Assert.True(isController);
- }
- }
-}
-
-// These controllers are used to test the DefaultActionDiscoveryConventions implementation
-// which REQUIRES that they be public top-level classes. To avoid having to stub out the
-// implementation of this class to test it, they are just top level classes. Don't reuse
-// these outside this test - find a better way or use nested classes to keep the tests
-// independent.
-namespace Microsoft.AspNet.Mvc.DefaultActionDiscoveryConventionsControllers
-{
- public abstract class AbstractController : Mvc.Controller
- {
- }
-
- public class DerivedAbstractController : AbstractController
- {
- }
-
- public class BaseController : Mvc.Controller
- {
- public void GetFromBase() // Valid action method.
- {
- }
-
- [NonAction]
- public virtual void OverridenNonActionMethod()
- {
- }
-
- [NonAction]
- public virtual void NewMethod()
- {
- }
-
- public override RedirectResult Redirect(string url)
- {
- return base.Redirect(url + "#RedirectOverride");
- }
- }
-
- public class DerivedController : BaseController
- {
- public void GetFromDerived() // Valid action method.
- {
- }
-
- [HttpGet]
- public override void OverridenNonActionMethod()
- {
- }
-
- public new void NewMethod() // Valid action method.
- {
- }
-
- public void GenericMethod()
- {
- }
-
- private void PrivateMethod()
- {
- }
-
- public static void StaticMethod()
- {
- }
-
- protected static void ProtectedStaticMethod()
- {
- }
-
- private static void PrivateStaticMethod()
- {
- }
- }
-
- public class Controller
- {
- }
-
- public class OpenGenericController
- {
- }
-
- public class DerivedGenericController : OpenGenericController
- {
- }
-
- public interface IController
- {
- }
-
- public class NoSuffix : Mvc.Controller
- {
- }
-
- public class PocoController
- {
- }
-
- public class OperatorOverloadingController : Mvc.Controller
- {
- public static OperatorOverloadingController operator +(
- OperatorOverloadingController c1,
- OperatorOverloadingController c2)
- {
- return new OperatorOverloadingController();
- }
- }
-
- public class NoRouteAttributeOnControllerController : Controller
- {
- [HttpGet("All")]
- [HttpPost("List")]
- public void Index() { }
-
- [HttpPost("Change")]
- public void Edit() { }
-
- public void Remove() { }
-
- [Route("Update")]
- public void Update() { }
-
- [AcceptVerbs("GET", "HEAD", Route = "ListAll")]
- public void List() { }
- }
-
- [Route("Products")]
- public class SingleRouteAttributeController : Controller
- {
- [HttpGet("All")]
- [HttpGet("List")]
- public void Index() { }
-
- public void Delete() { }
- }
-
- [Route("Products")]
- [Route("Items")]
- public class MultipleRouteAttributeController : Controller
- {
- [HttpGet("All")]
- [HttpGet("List")]
- public void Index() { }
-
- public void Delete() { }
- }
-
- // Here the constraints on the methods are acting as an IActionHttpMethodProvider and
- // not as an IRouteTemplateProvider given that there is no RouteAttribute
- // on the controller and the template for all the constraints on a method is null.
- public class ConventionallyRoutedController : Controller
- {
- public void Edit() { }
-
- [CustomHttpMethods("PUT", "PATCH")]
- public void Update() { }
-
- [HttpDelete]
- public void Delete() { }
-
- [HttpPost]
- [HttpGet]
- public void Details() { }
-
- [HttpGet]
- [HttpPut]
- [AcceptVerbs("GET", "POST")]
- public void List() { }
- }
-
- public class CustomHttpMethodsAttribute : Attribute, IActionHttpMethodProvider
- {
- private readonly string[] _methods;
-
- public CustomHttpMethodsAttribute(params string[] methods)
- {
- _methods = methods;
- }
-
- public IEnumerable HttpMethods
- {
- get
- {
- return _methods;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs
index 00c840f5be..a045a20674 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs
@@ -6,10 +6,13 @@ using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Threading.Tasks;
+using System.Reflection;
using Microsoft.AspNet.Http;
-using Microsoft.AspNet.Mvc.Routing;
-using Microsoft.AspNet.Routing;
+using Microsoft.AspNet.Mvc.ApplicationModel;
using Microsoft.AspNet.Mvc.Logging;
+using Microsoft.AspNet.Mvc.Routing;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.NestedProviders;
using Microsoft.Framework.Logging;
@@ -51,7 +54,7 @@ namespace Microsoft.AspNet.Mvc
Assert.Empty(values.ActionsMatchingRouteConstraints);
Assert.Empty(values.ActionsMatchingActionConstraints);
Assert.Empty(values.FinalMatches);
- Assert.Null(values.SelectedAction);
+ Assert.Null(values.SelectedAction);
Assert.DoesNotThrow(() => values.Summary);
}
@@ -563,7 +566,7 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var expectedMessage =
- "Multiple actions matched. " +
+ "Multiple actions matched. " +
"The following actions matched route data and had all constraints satisfied:" + Environment.NewLine +
Environment.NewLine +
"Ambiguous1" + Environment.NewLine +
@@ -595,6 +598,200 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(expectedMessage, ex.Message);
}
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PUT")]
+ [InlineData("POST")]
+ [InlineData("DELETE")]
+ [InlineData("PATCH")]
+ public async Task HttpMethodAttribute_ActionWithMultipleHttpMethodAttributeViaAcceptVerbs_ORsMultipleHttpMethods(string verb)
+ {
+ // Arrange
+ var routeContext = new RouteContext(GetHttpContext(verb));
+ routeContext.RouteData.Values = new Dictionary
+ {
+ { "controller", "HttpMethodAttributeTests_RestOnly" },
+ { "action", "Patch" }
+ };
+
+ // Act
+ var result = await InvokeActionSelector(routeContext);
+
+ // Assert
+ Assert.Equal("Patch", result.Name);
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PUT")]
+ [InlineData("POST")]
+ [InlineData("DELETE")]
+ [InlineData("PATCH")]
+ public async Task HttpMethodAttribute_ActionWithMultipleHttpMethodAttributes_ORsMultipleHttpMethods(string verb)
+ {
+ // Arrange
+ var routeContext = new RouteContext(GetHttpContext(verb));
+ routeContext.RouteData.Values = new Dictionary()
+ {
+ { "controller", "HttpMethodAttributeTests_RestOnly" },
+ { "action", "Put" }
+ };
+
+ // Act
+ var result = await InvokeActionSelector(routeContext);
+
+ // Assert
+ Assert.Equal("Put", result.Name);
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PUT")]
+ public async Task HttpMethodAttribute_ActionDecoratedWithHttpMethodAttribute_OverridesConvention(string verb)
+ {
+ // Arrange
+ // Note no action name is passed, hence should return a null action descriptor.
+ var routeContext = new RouteContext(GetHttpContext(verb));
+ routeContext.RouteData.Values = new Dictionary()
+ {
+ { "controller", "HttpMethodAttributeTests_RestOnly" },
+ };
+
+ // Act
+ var result = await InvokeActionSelector(routeContext);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("Put")]
+ [InlineData("RPCMethod")]
+ [InlineData("RPCMethodWithHttpGet")]
+ public void NonActionAttribute_ActionNotReachable(string actionName)
+ {
+ // Arrange
+ var actionDescriptorProvider = GetActionDescriptorProvider();
+
+ // Act
+ var result = actionDescriptorProvider.GetDescriptors()
+ .Select(x => x as ControllerActionDescriptor)
+ .FirstOrDefault(
+ x => x.ControllerName == "NonAction" &&
+ x.Name == actionName);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PUT")]
+ [InlineData("POST")]
+ [InlineData("DELETE")]
+ [InlineData("PATCH")]
+ public async Task ActionNameAttribute_ActionGetsExposedViaActionName_UnreachableByConvention(string verb)
+ {
+ // Arrange
+ var routeContext = new RouteContext(GetHttpContext(verb));
+ routeContext.RouteData.Values = new Dictionary
+ {
+ { "controller", "ActionName" },
+ { "action", "RPCMethodWithHttpGet" }
+ };
+
+ // Act
+ var result = await InvokeActionSelector(routeContext);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("GET", "CustomActionName_Verb")]
+ [InlineData("PUT", "CustomActionName_Verb")]
+ [InlineData("POST", "CustomActionName_Verb")]
+ [InlineData("DELETE", "CustomActionName_Verb")]
+ [InlineData("PATCH", "CustomActionName_Verb")]
+ [InlineData("GET", "CustomActionName_DefaultMethod")]
+ [InlineData("PUT", "CustomActionName_DefaultMethod")]
+ [InlineData("POST", "CustomActionName_DefaultMethod")]
+ [InlineData("DELETE", "CustomActionName_DefaultMethod")]
+ [InlineData("PATCH", "CustomActionName_DefaultMethod")]
+ [InlineData("GET", "CustomActionName_RpcMethod")]
+ [InlineData("PUT", "CustomActionName_RpcMethod")]
+ [InlineData("POST", "CustomActionName_RpcMethod")]
+ [InlineData("DELETE", "CustomActionName_RpcMethod")]
+ [InlineData("PATCH", "CustomActionName_RpcMethod")]
+ public async Task ActionNameAttribute_DifferentActionName_UsesActionNameFromActionNameAttribute(string verb, string actionName)
+ {
+ // Arrange
+ var routeContext = new RouteContext(GetHttpContext(verb));
+ routeContext.RouteData.Values = new Dictionary
+ {
+ { "controller", "ActionName" },
+ { "action", actionName }
+ };
+
+ // Act
+ var result = await InvokeActionSelector(routeContext);
+
+ // Assert
+ Assert.Equal(actionName, result.Name);
+ }
+
+ private async Task InvokeActionSelector(RouteContext context)
+ {
+ var actionDescriptorProvider = GetActionDescriptorProvider();
+ var descriptorProvider =
+ new NestedProviderManager(new[] { actionDescriptorProvider });
+
+ var serviceContainer = new ServiceContainer();
+ serviceContainer.AddService(typeof(INestedProviderManager),
+ descriptorProvider);
+
+ var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
+ var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
+
+ var actionConstraintProvider = new NestedProviderManager(
+ new INestedProvider[]
+ {
+ new DefaultActionConstraintProvider(serviceContainer),
+ });
+
+ var defaultActionSelector = new DefaultActionSelector(
+ actionCollectionDescriptorProvider,
+ decisionTreeProvider,
+ actionConstraintProvider,
+ NullLoggerFactory.Instance);
+
+ return await defaultActionSelector.SelectAsync(context);
+ }
+
+ private ControllerActionDescriptorProvider GetActionDescriptorProvider()
+ {
+ var assemblyProvider = new StaticAssemblyProvider();
+
+ var controllerTypes = typeof(DefaultActionSelectorTests)
+ .GetNestedTypes(BindingFlags.NonPublic)
+ .Select(t => t.GetTypeInfo());
+
+ var modelBuilder = new StaticControllerModelBuilder(controllerTypes.ToArray());
+
+ return new ControllerActionDescriptorProvider(
+ assemblyProvider,
+ modelBuilder,
+ new TestGlobalFilterProvider(),
+ new MockMvcOptionsAccessor());
+ }
+
+ private static HttpContext GetHttpContext(string httpMethod)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = httpMethod;
+ return httpContext;
+ }
+
private static ActionDescriptor[] GetActions()
{
return new ActionDescriptor[]
@@ -647,9 +844,9 @@ namespace Microsoft.AspNet.Mvc
});
return new DefaultActionSelector(
- actionProvider.Object,
- decisionTreeProvider,
- actionConstraintProvider,
+ actionProvider.Object,
+ decisionTreeProvider,
+ actionConstraintProvider,
loggerFactory);
}
@@ -762,5 +959,79 @@ namespace Microsoft.AspNet.Mvc
callNext();
}
}
+
+ private class NonActionController
+ {
+ [NonAction]
+ public void Put()
+ {
+ }
+
+ [NonAction]
+ public void RPCMethod()
+ {
+ }
+
+ [NonAction]
+ [HttpGet]
+ public void RPCMethodWithHttpGet()
+ {
+ }
+ }
+
+ private class HttpMethodAttributeTests_DefaultMethodValidationController
+ {
+ public void Index()
+ {
+ }
+
+ // Method with custom attribute.
+ [HttpGet]
+ public void Get()
+ { }
+
+ // InvalidMethod ( since its private)
+ private void Post()
+ { }
+ }
+
+ private class ActionNameController
+ {
+ [ActionName("CustomActionName_Verb")]
+ public void Put()
+ {
+ }
+
+ [ActionName("CustomActionName_DefaultMethod")]
+ public void Index()
+ {
+ }
+
+ [ActionName("CustomActionName_RpcMethod")]
+ public void RPCMethodWithHttpGet()
+ {
+ }
+ }
+
+ private class HttpMethodAttributeTests_RestOnlyController
+ {
+ [HttpGet]
+ [HttpPut]
+ [HttpPost]
+ [HttpDelete]
+ [HttpPatch]
+ public void Put()
+ {
+ }
+
+ [AcceptVerbs("PUT", "post", "GET", "delete", "pATcH")]
+ public void Patch()
+ {
+ }
+ }
+
+ private class HttpMethodAttributeTests_DerivedController : HttpMethodAttributeTests_RestOnlyController
+ {
+ }
}
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs b/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs
index fd2f685acf..90547adeb8 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs
@@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc
{
get
{
- yield return typeof(StaticActionDiscoveryConventions).GetTypeInfo().Assembly;
+ yield return typeof(StaticAssemblyProvider).GetTypeInfo().Assembly;
}
}
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/StaticActionDiscoveryConventions.cs b/test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerModelBuilder.cs
similarity index 59%
rename from test/Microsoft.AspNet.Mvc.Core.Test/StaticActionDiscoveryConventions.cs
rename to test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerModelBuilder.cs
index 9a1e8c2e31..6b3deca884 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/StaticActionDiscoveryConventions.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerModelBuilder.cs
@@ -5,22 +5,23 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
-namespace Microsoft.AspNet.Mvc
+namespace Microsoft.AspNet.Mvc.ApplicationModel
{
///
- /// An implementation of DefaultActionDiscoveryConventions that only allows controllers
+ /// An implementation of StaticControllerModelBuilder that only allows controllers
/// from a fixed set of types.
///
- public class StaticActionDiscoveryConventions : DefaultActionDiscoveryConventions
+ public class StaticControllerModelBuilder : DefaultControllerModelBuilder
{
- public StaticActionDiscoveryConventions(params TypeInfo[] controllerTypes)
+ public StaticControllerModelBuilder(params TypeInfo[] controllerTypes)
+ : base(new DefaultActionModelBuilder())
{
ControllerTypes = new List(controllerTypes ?? Enumerable.Empty());
}
public List ControllerTypes { get; private set; }
- public override bool IsController([NotNull]TypeInfo typeInfo)
+ protected override bool IsController([NotNull] TypeInfo typeInfo)
{
return ControllerTypes.Contains(typeInfo);
}
diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs
index c37bb5aee8..ad0eaa3951 100644
--- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs
+++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Mvc.ApplicationModel;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.WebApiCompatShim;
using Microsoft.Framework.DependencyInjection;
@@ -345,8 +346,6 @@ namespace System.Web.Http
.SetupGet(fp => fp.Filters)
.Returns(new List());
- var conventions = new NamespaceLimitedActionDiscoveryConventions();
-
var options = new MvcOptions();
var setup = new WebApiCompatShimOptionsSetup();
@@ -359,7 +358,7 @@ namespace System.Web.Http
var provider = new ControllerActionDescriptorProvider(
assemblyProvider.Object,
- conventions,
+ new NamespaceLimitedActionDiscoveryConventions(),
filterProvider.Object,
optionsAccessor.Object);
@@ -370,9 +369,14 @@ namespace System.Web.Http
});
}
- private class NamespaceLimitedActionDiscoveryConventions : DefaultActionDiscoveryConventions
+ private class NamespaceLimitedActionDiscoveryConventions : DefaultControllerModelBuilder
{
- public override bool IsController(TypeInfo typeInfo)
+ public NamespaceLimitedActionDiscoveryConventions()
+ : base(new DefaultActionModelBuilder())
+ {
+ }
+
+ protected override bool IsController(TypeInfo typeInfo)
{
return
typeInfo.Namespace == "System.Web.Http.TestControllers" &&