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" &&